diff --git a/apps/tools.js b/apps/tools.js index 67ba511..6520d7e 100644 --- a/apps/tools.js +++ b/apps/tools.js @@ -85,6 +85,7 @@ import { truncateString, urlTransformShortLink } from "../utils/common.js"; +import { convertFlvToMp4 } from "../utils/ffmpeg-util.js"; import { checkAndRemoveFile, deleteFolderRecursive, getMediaFilesAndOthers, mkdirIfNotExists } from "../utils/file.js"; import GeneralLinkAdapter from "../utils/general-link-adapter.js"; import { LagrangeAdapter } from "../utils/lagrange-adapter.js"; @@ -260,6 +261,8 @@ export class tools extends plugin { this.douyinCompression = this.toolsConfig.douyinCompression; // 加载抖音是否开启评论 this.douyinComments = this.toolsConfig.douyinComments; + // 加载抖音的是否开启兼容模式 + this.douyinStreamCompatibility = this.toolsConfig.douyinStreamCompatibility; // 加载小红书Cookie this.xiaohongshuCookie = this.toolsConfig.xiaohongshuCookie; // 翻译引擎 @@ -352,7 +355,6 @@ export class tools extends plugin { const webcastResp = await fetch(dyApi); const webcastData = await webcastResp.json(); const item = webcastData.data.room; - logger.info(item); const { title, cover, user_count, stream_url } = item; const dySendContent = `${ this.identifyPrefix }识别:抖音直播,${ title }` e.reply([segment.image(cover?.url_list?.[0]), dySendContent, `\n🏄‍♂️在线人数:${ user_count }人正在观看`]); @@ -481,8 +483,13 @@ export class tools extends plugin { * @param second */ async sendStreamSegment(e, stream_url, second = this.streamDuration) { - const outputFilePath = `${ this.getCurDownloadPath(e) }/stream_${second}s.flv`; - await checkAndRemoveFile(outputFilePath); + let outputFilePath = `${ this.getCurDownloadPath(e) }/stream_${second}s.flv`; + // 删除临时文件 + if (this.douyinStreamCompatibility) { + await checkAndRemoveFile(outputFilePath.replace("flv", "mp4")); + } else { + await checkAndRemoveFile(outputFilePath); + } // 创建一个取消令牌 const CancelToken = axios.CancelToken; @@ -502,9 +509,17 @@ export class tools extends plugin { setTimeout(async () => { logger.info(`[R插件][发送直播流] 直播下载 ${ second } 秒钟到,停止下载!`); // 取消请求 - source.cancel('下载时间到,停止请求'); + source.cancel('[R插件][发送直播流] 下载时间到,停止请求'); response.data.unpipe(file); // 取消管道连接 file.end(); // 结束写入 + // 这里判断是否开启兼容模式 + if (this.douyinStreamCompatibility) { + logger.info(`[R插件][发送直播流] 开启兼容模式,开始转换mp4格式...`); + const resolvedOutputPath = await convertFlvToMp4(outputFilePath, outputFilePath.replace(".flv", ".mp4")); + fs.unlinkSync(outputFilePath); + outputFilePath = resolvedOutputPath; + logger.info(`[R插件][发送直播流] 转换完成,开始发送视频...`); + } await this.sendVideoToUpload(e, outputFilePath); }, second * 1000); diff --git a/config/tools.yaml b/config/tools.yaml index cc46cfe..0785d9a 100644 --- a/config/tools.yaml +++ b/config/tools.yaml @@ -26,6 +26,7 @@ YouTubeGraphicsOptions: 720 #YouTobe的下载画质,0为原画,1080,720, douyinCookie: '' # douyin's cookie, 格式:odin_tt=xxx;passport_fe_beating_status=xxx;sid_guard=xxx;uid_tt=xxx;uid_tt_ss=xxx;sid_tt=xxx;sessionid=xxx;sessionid_ss=xxx;sid_ucp_v1=xxx;ssid_ucp_v1=xxx;passport_assist_user=xxx;ttwid=xxx; douyinCompression: true # true-压缩,false-不压缩;是否使用压缩视频格式的抖音(默认使用),使用后加速视频发送 douyinComments: false # true-开启评论,false-关闭评论 +douyinStreamCompatibility: false # 兼容模式,NCQQ不用开,其他ICQQ、LLO需要开启 xiaohongshuCookie: '' # 2024-8-2后反馈必须使用ck,不然无法解析 diff --git a/guoba.support.js b/guoba.support.js index d87e661..a62f977 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -245,6 +245,12 @@ export function supportGuoba() { component: "Switch", required: false, }, + { + field: "tools.douyinStreamCompatibility", + label: "抖音直播是否开启兼容模式", + component: "Switch", + required: false, + }, { field: "tools.xiaohongshuCookie", label: "小红书的Cookie", diff --git a/utils/ffmpeg-util.js b/utils/ffmpeg-util.js index df277ad..27ffa1e 100644 --- a/utils/ffmpeg-util.js +++ b/utils/ffmpeg-util.js @@ -1,5 +1,6 @@ import path from 'path'; import { exec } from 'child_process'; +import fs from "fs"; /** * 提取关键帧 @@ -34,22 +35,32 @@ export async function extractKeyframes(inputFilePath, outputFolderPath, frameCou * @param {string} outputFilePath - 输出的 MP4 文件路径 * @returns {Promise} - 返回一个 Promise,成功时返回输出文件路径,失败时返回错误信息 */ -function convertFlvToMp4(inputFilePath, outputFilePath) { +export function convertFlvToMp4(inputFilePath, outputFilePath) { return new Promise((resolve, reject) => { - const command = `ffmpeg -i ${inputFilePath} ${outputFilePath}`; + const resolvedInputPath = path.resolve(inputFilePath); + const resolvedOutputPath = path.resolve(outputFilePath); - logger.info(`[R插件][ffmpeg工具]执行命令:${command}`); - - exec(command, (error, stdout, stderr) => { - if (error) { - reject(`执行 ffmpeg 命令时出错: ${error.message}`); + // 检查文件是否存在 + fs.access(resolvedInputPath, fs.constants.F_OK, (err) => { + if (err) { + reject(`[R插件][ffmpeg工具]输入文件不存在: ${resolvedInputPath}`); return; } - if (stderr) { - reject(`ffmpeg 标准错误输出: ${stderr}`); - return; - } - resolve(outputFilePath); + + const command = `ffmpeg -y -i "${resolvedInputPath}" "${resolvedOutputPath}"`; + logger.info(`[R插件][ffmpeg工具]执行命令:${command}`); + + // 执行 ffmpeg 转换 + exec(command, (error, stdout, stderr) => { + if (error) { + reject(`[R插件][ffmpeg工具]执行 ffmpeg 命令时出错: ${error.message}`); + return; + } + if (stderr) { + logger.warn(`[R插件][ffmpeg工具]ffmpeg 标准错误输出: ${stderr}`); + } + resolve(resolvedOutputPath); + }); }); }); }