biliob/src/main/java/com/yutou/bilibili/services/LiveVideoDownloadService.java

483 lines
20 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.yutou.bilibili.services;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.util.DateUtils;
import com.yutou.biliapi.api.LiveApi;
import com.yutou.biliapi.bean.live.LiveRoomConfig;
import com.yutou.biliapi.bean.live.LiveRoomInfo;
import com.yutou.biliapi.bean.live.LiveRoomPlayInfo;
import com.yutou.biliapi.bean.live.database.LiveConfigDatabaseBean;
import com.yutou.biliapi.bean.live.database.LiveVideoDatabaseBean;
import com.yutou.biliapi.bean.login.LoginCookieDatabaseBean;
import com.yutou.biliapi.databases.BiliBiliLoginDatabase;
import com.yutou.biliapi.databases.BiliLiveConfigDatabase;
import com.yutou.biliapi.databases.BiliLiveDatabase;
import com.yutou.biliapi.enums.LiveProtocol;
import com.yutou.biliapi.enums.LiveVideoCodec;
import com.yutou.biliapi.enums.LiveVideoDefinition;
import com.yutou.biliapi.enums.LiveVideoFormat;
import com.yutou.biliapi.net.BiliLiveNetApiManager;
import com.yutou.biliapi.net.WebSocketServer;
import com.yutou.bilibili.Tools.DateFormatUtils;
import com.yutou.bilibili.Tools.FileServerUtils;
import com.yutou.bilibili.datas.VideoFilePath;
import com.yutou.bilibili.interfaces.DownloadInterface;
import com.yutou.common.okhttp.HttpCallback;
import com.yutou.common.okhttp.HttpDownloadUtils;
import com.yutou.common.record.AbsVideoRecord;
import com.yutou.common.utils.ConfigTools;
import com.yutou.common.utils.FFmpegUtils;
import com.yutou.common.utils.Log;
import jakarta.annotation.Resource;
import okhttp3.Headers;
import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static com.alibaba.fastjson2.util.DateUtils.DateTimeFormatPattern.DATE_FORMAT_10_DASH;
@Service
public class LiveVideoDownloadService {
private final ThreadPoolExecutor executor;
private final List<String> userStopList = new ArrayList<>();//手动停止列表
private final AbsVideoRecord videoRecord;
@Resource
LiveDatabasesService liveDatabasesService;
@Resource
WebSocketServer webSocketServer;
public LiveVideoDownloadService() {
Log.i("初始化下载服务");
videoRecord = new FFmpegUtils();
executor = new ThreadPoolExecutor(2, 4, Long.MAX_VALUE, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));
}
public boolean checkDownload(String roomId) {
return videoRecord.check(roomId);
}
public void clearUserStopList() {
userStopList.clear();
}
public void removeUserStopList(String roomId) {
userStopList.remove(roomId);
}
public boolean isUserStopList(String roomId) {
return userStopList.contains(roomId);
}
public void start(LiveConfigDatabaseBean bean, boolean isUser) {
if (!isUser && userStopList.contains(bean.getRoomId())) {
return;
}
if (isUser) {
userStopList.remove(bean.getRoomId());
}
if (videoRecord.check(bean.getRoomId())) {
return;
}
BiliLiveNetApiManager.getInstance().getApi(bean.getRecordUid()).getRoomInfo(bean.getRoomId()).enqueue(new HttpCallback<LiveRoomInfo>() {
@Override
public void onResponse(Headers headers, int code, String status, LiveRoomInfo response, String rawResponse) {
if (response.getLiveStatus() == 1) {
if (bean.getKeywordList()!=null&&!bean.getKeywordList().isEmpty() && !isUser) {
String foundKey = bean.getKeywordList().stream()
.filter(key -> response.getTitle().contains(key))
.findFirst()
.orElse(null);
if (foundKey != null) {
Log.i(response.getRoomId(), "检测到开播关键词", foundKey, response.getTitle());
} else {
Log.i(response.getRoomId(), "未检测到关键词", response.getTitle(), Arrays.toString(bean.getKeywordList().toArray()));
return;
}
}
VideoTask task = new VideoTask(bean, response);
executor.execute(task);
} else {
Log.i(bean.getRoomId(), "没有开播");
}
}
@Override
public void onFailure(Throwable throwable) {
Log.e(throwable, "移除下载");
}
});
}
public void stop(String roomId, boolean isUser) {
if (isUser) {
userStopList.add(roomId);
}
videoRecord.kill(roomId);
}
public JSONArray getDownloadTasks() {
JSONArray array = new JSONArray();
array.addAll(videoRecord.getRoomIds());
return array;
}
public void stopAll() {
videoRecord.killAll();
}
private class VideoTask implements Runnable {
LiveConfigDatabaseBean bean;
boolean isDownload = true;
LiveApi api;
String savePath;
File rootPath;
LiveConfigDatabaseBean config;
LiveVideoDatabaseBean videoDatabaseBean = null;
LiveRoomInfo roomInfo;
public VideoTask(LiveConfigDatabaseBean bean, LiveRoomInfo roomInfo) {
this.bean = bean;
this.roomInfo = roomInfo;
api = BiliLiveNetApiManager.getInstance().getApi(bean.getRecordUid());
}
@Override
public void run() {
if (roomInfo.getLiveStatus() == 1) {
String time = DateUtils.format(new Date().getTime(), DATE_FORMAT_10_DASH);
rootPath = new File(bean.getRecordPath() + File.separator + bean.getAnchorName() + File.separator + time + File.separator + "[" +
DateUtils.format(new Date(),
"HH-mm-ss") + "]" + roomInfo.getTitle());
savePath = rootPath.getAbsolutePath() + File.separator + roomInfo.getTitle() + ".m3u8";
if (!rootPath.exists()) {
rootPath.mkdirs();
}
record(bean, roomInfo);
} else {
stop();
}
}
private void stop() {
videoRecord.kill(bean.getRoomId());
api.getRoomInfo(config.getRoomId()).enqueue(new HttpCallback<LiveRoomInfo>() {
@Override
public void onResponse(Headers headers, int code, String status, LiveRoomInfo response, String rawResponse) {
if (response.getLiveStatus() == 1) {
LiveVideoDownloadService.this.start(bean, false);
} else {
LiveVideoDownloadService.this.stop(bean.getRoomId(), false);
}
}
@Override
public void onFailure(Throwable throwable) {
}
});
}
private void record(LiveConfigDatabaseBean bean, LiveRoomInfo roomInfo) {
this.config = bean;
isDownload = true;
LiveRoomConfig config = new LiveRoomConfig();
config.setLoginUid(bean.getRecordUid());
config.setRoomId(bean.getRoomId());
config.setAnchorName(bean.getAnchorName());
config.setLogin(StringUtils.hasText(bean.getRecordUid()));
config.setRoomInfo(roomInfo);
config.setRootPath(bean.getRecordPath());
saveLiveInfo(roomInfo);
api.getLiveRoomPlayInfo(
bean.getRoomId(),
LiveProtocol.getAll(),
LiveVideoFormat.getAll(),
LiveVideoCodec.getAll(),
LiveVideoDefinition.ORIGINAL.getValue()).enqueue(new HttpCallback<LiveRoomPlayInfo>() {
@Override
public void onResponse(Headers headers, int code, String status, LiveRoomPlayInfo response, String rawResponse) {
Random random = new Random();
List<LiveRoomPlayInfo.Stream> streams = response.getPlayurlInfo().getPlayurl().getStream();
List<LiveRoomPlayInfo.Stream> streamList = streams.stream()
.filter(it -> "http_stream".equals(it.getProtocolName()))
.toList();
LiveRoomPlayInfo.Stream stream = streamList.get(random.nextInt(streamList.size()));
if (stream == null) {
return;
}
List<LiveRoomPlayInfo.Format> formats = stream.getFormat().stream().filter(it -> "flv".equals(it.getFormatName())).toList();
LiveRoomPlayInfo.Format format = formats.get(random.nextInt(formats.size()));
List<LiveRoomPlayInfo.Codec> codecs = format.getCodec().stream().filter(item -> "avc".equals(item.getCodecName())).toList();
LiveRoomPlayInfo.Codec codec = codecs.get(random.nextInt(codecs.size()));
int urlIndex = random.nextInt(codec.getUrlInfo().size());
LiveRoomPlayInfo.UrlInfo urlInfo = codec.getUrlInfo().get(urlIndex);
String url = urlInfo.getHost() + codec.getBaseUrl() + urlInfo.getExtra();
Log.i("下载直播", rawResponse, codec.toString(), urlInfo.toString(), "URL:" + url);
if (bean.getRecordLiveModel() == 1) {
javaRecord(url, response);
} else {
ffmpeg(url, response);
}
}
@Override
public void onFailure(Throwable throwable) {
Log.e(throwable);
}
});
}
private void javaRecord(String url, LiveRoomPlayInfo playInfo) {
HttpDownloadUtils.download(new HttpDownloadUtils.Builder()
.setUrl(url)
.setPath(savePath)
.setDownloadInterface(new DownloadInterface() {
@Override
public void onDownloadStart() {
super.onDownloadStart();
VideoTask.this.onStart();
}
@Override
public boolean onDownloading(double soFarBytes, double totalBytes) {
return isDownload;
}
@Override
public void onDownload(File file) {
super.onDownload(file);
stop();
}
@Override
public void onError(Exception e) {
super.onError(e);
stop();
}
}));
}
private void ffmpeg(String url, LiveRoomPlayInfo playInfo) {
String ffmpegPath = ConfigTools.load(ConfigTools.CONFIG, "ffmpeg", String.class);
String cookie = "";
LoginCookieDatabaseBean ck = BiliBiliLoginDatabase.getInstance().getCookie(config.getRecordUid());
if (ck != null) {
cookie = ck.toCookieString();
}
FFmpegUtils.Builder builder = new FFmpegUtils.Builder()
.withParam("-user_agent", ConfigTools.getUserAgent())
.withParam("-headers", "Referer: https://live.bilibili.com/" + playInfo.getRoomId())
// .withNotSymbolParam("-reconnect", "1")
// .withNotSymbolParam("-reconnect_at_eof", "1")
// .withNotSymbolParam("-reconnect_streamed", "1")
// .withNotSymbolParam("-reconnect_delay_max", "2")
// .withNotSymbolParam("-loglevel", "error")
// .withNotSymbolParam("-progress", "-")
// .withNotSymbolParam("-fflags", "+genpts")
// .withNotSymbolParam("-threads", "8")//看bili-go也没有加这个改成设置好了
// .withNotSymbolParam("-bufsize", "10M")
.withNotSymbolParam("-f", "segment")
.withNotSymbolParam("-rw_timeout", "60000000")
.withNotSymbolParam("-segment_time", "60")
.withNotSymbolParam("-segment_format", "mpegts")
.withNotSymbolParam("-map", "0")
.withParam("-segment_list", savePath)
.withNotSymbolParam("-c", "copy")
.withNotSymbolParam("-bsf:a", "aac_adtstoasc")
// .withNotSymbolParam("-loglevel", "debug")
.withNotSymbolParam("-y", "")
// .withNotSymbolParam("-progress",new File("cache",config.getRoomId()+".txt").getAbsolutePath()); //输出进度日志,暂时没啥用
;
if (ck != null) {
// builder = builder.withParam("-cookies", cookie);
}
FFmpegUtils command = builder.build(config.getRoomId(), ffmpegPath, url, savePath.replace(".m3u8", "-%04d.ts"));
Log.i(command.getCommandDecode());
try {
command.start(new DownloadInterface() {
TimerTask task = null;
@Override
public void onDownloadStart() {
super.onDownloadStart();
task = new TimerTask() {
@Override
public void run() {
VideoTask.this.onStart();
Log.i("启动录制:" + playInfo.getRoomId());
task = null;
cancel();
}
};
new Timer().schedule(task, 5 * 1000);
}
@Override
public boolean onDownloading(double soFarBytes, double totalBytes) {
return super.onDownloading(soFarBytes, totalBytes);
}
@Override
public void onDownload(File file) {
super.onDownload(file);
if (task != null) {
task.cancel();
task = null;
try {
FileUtils.deleteDirectory(rootPath);
} catch (IOException e) {
Log.i(rootPath.getAbsolutePath());
Log.e(e);
}
}
if (videoDatabaseBean != null) {
videoDatabaseBean.setStopTime(new Date());
liveDatabasesService.getLiveDatabase(bean.getRoomId()).addLiveInfo(videoDatabaseBean);
}
stopRecordDanmu();
}
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void onStart() {
videoDatabaseBean = new LiveVideoDatabaseBean();
videoDatabaseBean.setPath(savePath);
videoDatabaseBean.setRoomInfoJson(JSONObject.toJSONString(roomInfo));
videoDatabaseBean.setStartTime(new Date());
liveDatabasesService.getLiveDatabase(bean.getRoomId()).addLiveInfo(videoDatabaseBean);
recordDanmu();
// LiveInfoNfoTools.saveLiveInfoNfo(roomInfo, rootPath.getAbsolutePath(), new File(savePath).getName().replace(".flv", ".nfo"));
}
//录制弹幕
private void recordDanmu() {
if (bean.isSyncDanmuForLive() && !webSocketServer.checkRoom(liveDatabasesService.buildConfig(bean.getRoomId()))) {
webSocketServer.addRoom(liveDatabasesService.buildConfig(bean.getRoomId()), true);
}
}
private void stopRecordDanmu() {
if (bean.isSyncDanmuForLive() && webSocketServer.checkRoom(liveDatabasesService.buildConfig(bean.getRoomId()))) {
webSocketServer.stopRoom(bean.getRoomId(), false);
}
}
}
private void saveLiveInfo(LiveRoomInfo roomInfo) {
}
public VideoFilePath getVideoPath(String roomId) {
LiveConfigDatabaseBean bean = liveDatabasesService.getConfigDatabase().getConfig(roomId);
return getVideoFilePath(bean);
}
private VideoFilePath getVideoFilePath(LiveConfigDatabaseBean configBean) {
String recordPath = configBean.getRecordPath() + File.separator + configBean.getAnchorName();
File recordDir = new File(recordPath);
VideoFilePath path = createVideoRootFilePath(configBean, recordDir);
if (recordDir.exists()) {
List<LiveVideoDatabaseBean> infos = liveDatabasesService.getLiveDatabase(configBean.getRoomId()).getLiveInfos();
path.setChildren(getVideoInfo(infos));
}
return path;
}
private VideoFilePath createVideoRootFilePath(LiveConfigDatabaseBean config, File db) {
VideoFilePath path = new VideoFilePath();
path.setRoomId(config.getRoomId());
path.setCover(config.getAnchorFace());
path.setName(config.getAnchorName());
path.setUid(config.getAnchorUid());
path.setParent(true);
path.setPath(FileServerUtils.toUrl(db.getParent()));
return path;
}
private Map<String, List<VideoFilePath>> getVideoInfo(List<LiveVideoDatabaseBean> videoList) {
Map<String, List<VideoFilePath>> map = new HashMap<>();
for (LiveVideoDatabaseBean bean : videoList) {
String date = DateFormatUtils.getInstance().format(bean.getSql_time(), "yyyy-MM-dd");
if (!map.containsKey(date)) {
map.put(date, new ArrayList<>());
}
VideoFilePath path = new VideoFilePath();
LiveRoomInfo roomInfo = JSONObject.parseObject(bean.getRoomInfoJson(), LiveRoomInfo.class);
path.setRoomId(roomInfo.getRoomId());
path.setName(roomInfo.getTitle());
path.setUid(roomInfo.getUid());
path.setPath(String.valueOf(bean.getSql_time().getTime()));
path.setCover(roomInfo.getKeyframe());
path.setParent(false);
path.setChildren(null);
map.get(date).add(path);
}
return map;
}
public String getVideoPlay(String roomId, String videoId) {
String ffmpegPath = ConfigTools.load(ConfigTools.CONFIG, "ffmpeg", String.class);
LiveConfigDatabaseBean config = liveDatabasesService.getConfigDatabase().getConfig(roomId);
String recordPath = config.getRecordPath() + File.separator + config.getAnchorName();
LiveVideoDatabaseBean videoInfo = null;
for (LiveVideoDatabaseBean info : liveDatabasesService.getLiveDatabase(roomId).getLiveInfos()) {
if (videoId.trim().equals(String.valueOf(info.getSql_time().getTime()))) {
videoInfo = info;
break;
}
}
if (videoInfo != null) {
File videoFile = new File(videoInfo.getPath().replace("-%04d.ts", ".m3u8"));
if (!videoFile.exists()) {
videoFile = new File(videoInfo.getPath());
} else {
return videoInfo.getPath().replace(new File("live").getAbsolutePath(), "").replace(File.separator, "/").replace("-%04d.ts", ".m3u8");
}
FFmpegUtils ffmpeg = FFmpegUtils.segment(videoId, ffmpegPath, videoFile, ConfigTools.load(ConfigTools.CONFIG, "outVideoPath", String.class));
System.out.println(ffmpeg.getCommandDecode());
ffmpeg.start(new DownloadInterface() {
@Override
public void onDownload(File file) {
super.onDownload(file);
}
});
return ffmpeg.getOutputFilePath();
}
return null;
}
public static void main(String[] args) {
LiveVideoDownloadService service = new LiveVideoDownloadService();
String play = service.getVideoPlay("17961", "1730363029293");
System.out.println(play);
}
}