483 lines
20 KiB
Java
483 lines
20 KiB
Java
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);
|
||
}
|
||
}
|