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 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(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() { @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() { @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() { @Override public void onResponse(Headers headers, int code, String status, LiveRoomPlayInfo response, String rawResponse) { Random random = new Random(); List streams = response.getPlayurlInfo().getPlayurl().getStream(); List 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 formats = stream.getFormat().stream().filter(it -> "flv".equals(it.getFormatName())).toList(); LiveRoomPlayInfo.Format format = formats.get(random.nextInt(formats.size())); List 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 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> getVideoInfo(List videoList) { Map> 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); } }