From de6c8a55589d6ebbe698e1b8311ced150750b0f8 Mon Sep 17 00:00:00 2001 From: linyuchen Date: Wed, 13 Mar 2024 04:22:11 +0800 Subject: [PATCH] feat: send video by videoElement --- src/common/utils.ts | 81 +++++++++++++++++++++- src/ntqqapi/constructor.ts | 110 ++++++++++++++---------------- src/ntqqapi/hook.ts | 1 + src/ntqqapi/types.ts | 2 +- src/onebot11/action/BaseAction.ts | 4 +- src/onebot11/action/SendMsg.ts | 5 -- 6 files changed, 135 insertions(+), 68 deletions(-) diff --git a/src/common/utils.ts b/src/common/utils.ts index c6a1c75..9268cf2 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -4,6 +4,7 @@ import {ConfigUtil} from "./config"; import util from "util"; import {encode, getDuration, isWav} from "silk-wasm"; import fs from 'fs'; +import * as crypto from 'crypto'; import {v4 as uuidv4} from "uuid"; import ffmpeg from "fluent-ffmpeg" @@ -234,9 +235,9 @@ export async function encodeSilk(filePath: string) { } else { const pcm = fs.readFileSync(filePath); let duration = 0; - try{ + try { duration = getDuration(pcm); - }catch (e) { + } catch (e) { log("获取语音文件时长失败", filePath, e.stack) duration = fs.statSync(filePath).size / 1024 / 3 // 每3kb大约1s duration = Math.floor(duration) @@ -256,6 +257,80 @@ export async function encodeSilk(filePath: string) { } } +export async function getVideoInfo(filePath: string) { + const size = fs.statSync(filePath).size; + return new Promise<{ width: number, height: number, time: number, format: string, size: number, filePath: string }>((resolve, reject) => { + ffmpeg.ffprobe(filePath, (err, metadata) => { + if (err) { + reject(err); + } else { + const videoStream = metadata.streams.find(s => s.codec_type === 'video'); + if (videoStream) { + console.log(`视频尺寸: ${videoStream.width}x${videoStream.height}`); + } else { + console.log('未找到视频流信息。'); + } + resolve({ + width: videoStream.width, height: videoStream.height, + time: parseInt(videoStream.duration), + format: metadata.format.format_name, + size, + filePath + }); + } + }); + }) +} + +export async function encodeMp4(filePath: string) { + let videoInfo = await getVideoInfo(filePath); + log("视频信息", videoInfo) + if (videoInfo.format.indexOf("mp4") === -1) { + log("视频需要转换为MP4格式", filePath) + // 转成mp4 + const newPath: string = await new Promise((resolve, reject) => { + const newPath = filePath + ".mp4" + ffmpeg(filePath) + .toFormat('mp4') + .on('error', (err) => { + reject(`转换视频格式失败: ${err.message}`); + }) + .on('end', () => { + log('视频转换为MP4格式完成'); + resolve(newPath); // 返回转换后的文件路径 + }) + .save(newPath); + }); + return await getVideoInfo(newPath) + } + return videoInfo +} + export function isNull(value: any) { return value === undefined || value === null; -} \ No newline at end of file +} + + +export function calculateFileMD5(filePath: string): Promise { + return new Promise((resolve, reject) => { + // 创建一个流式读取器 + const stream = fs.createReadStream(filePath); + const hash = crypto.createHash('md5'); + + stream.on('data', (data: Buffer) => { + // 当读取到数据时,更新哈希对象的状态 + hash.update(data); + }); + + stream.on('end', () => { + // 文件读取完成,计算哈希 + const md5 = hash.digest('hex'); + resolve(md5); + }); + + stream.on('error', (err: Error) => { + // 处理可能的读取错误 + reject(err); + }); + }); +} diff --git a/src/ntqqapi/constructor.ts b/src/ntqqapi/constructor.ts index eb52616..28ce031 100644 --- a/src/ntqqapi/constructor.ts +++ b/src/ntqqapi/constructor.ts @@ -12,8 +12,9 @@ import { SendVideoElement } from "./types"; import {NTQQApi} from "./ntcall"; -import {encodeSilk, isGIF, log} from "../common/utils"; -import * as fs from "node:fs"; +import {calculateFileMD5, encodeSilk, getVideoInfo, isGIF, log, sleep} from "../common/utils"; +import {promises as fs} from "node:fs"; +import ffmpeg from "fluent-ffmpeg" export class SendMsgElementConstructor { @@ -88,8 +89,6 @@ export class SendMsgElementConstructor { } static async file(filePath: string, fileName: string = ""): Promise { - let picHeight = 0; - let picWidth = 0; const {md5, fileName: _fileName, path, fileSize} = await NTQQApi.uploadFile(filePath, ElementType.FILE); if (fileSize === 0) { throw "文件异常,大小为0"; @@ -108,13 +107,44 @@ export class SendMsgElementConstructor { } static async video(filePath: string, fileName: string = ""): Promise { - const {md5, fileName: _fileName, path, fileSize} = await NTQQApi.uploadFile(filePath, ElementType.VIDEO); + let {fileName: _fileName, path, fileSize, md5} = await NTQQApi.uploadFile(filePath, ElementType.VIDEO); if (fileSize === 0) { throw "文件异常,大小为0"; } - log("上传视频", md5, path, fileSize, fileName || _fileName) + // const videoInfo = await encodeMp4(path); + // path = videoInfo.filePath + // md5 = videoInfo.md5; + // fileSize = videoInfo.size; + // log("上传视频", md5, path, fileSize, fileName || _fileName) + const pathLib = require("path"); + let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`) + thumb = pathLib.dirname(thumb) + // log("thumb 目录", thumb) + const videoInfo = await getVideoInfo(path); + // log("视频信息", videoInfo) + const createThumb = new Promise((resolve, reject) => { + const thumbFileName = `${md5}_0.png` + ffmpeg(filePath) + .on("end", () => { + }) + .on("error", (err) => { + reject(err); + }) + .screenshots({ + timestamps: [0], + filename: thumbFileName, + folder: thumb, + size: videoInfo.width + "x" + videoInfo.height + }).on("end", () => { + resolve(pathLib.join(thumb, thumbFileName)); + }); + }) let thumbPath = new Map() - thumbPath.set(0, "E:\\SystemDocuments\\QQ\\721011692\\nt_qq\\nt_data\\Video\\2024-03\\Thumb\\8950eb327e26c01e69d4a0fab7e2b159_0.png") + const _thumbPath = await createThumb; + const thumbSize = (await fs.stat(_thumbPath)).size; + // log("生成缩略图", _thumbPath) + thumbPath.set(0, _thumbPath) + const thumbMd5 = await calculateFileMD5(_thumbPath); let element: SendVideoElement = { elementType: ElementType.VIDEO, elementId: "", @@ -122,59 +152,26 @@ export class SendMsgElementConstructor { fileName: fileName || _fileName, filePath: path, videoMd5: md5, - "thumbMd5": "9eee9e9a07b193cbaf4846522b0197b4", - fileTime: 15, + thumbMd5, + fileTime: videoInfo.time, thumbPath: thumbPath, - "thumbSize": 368286, - "fileFormat": 2, - "thumbWidth": 540, - "thumbHeight": 960, - // "busiType": 0, - // "subBusiType": 0, - // fileUuid: md5, + thumbSize, + thumbWidth: videoInfo.width, + thumbHeight: videoInfo.height, fileSize: "" + fileSize, - "transferStatus": 0, - "progress": 0, - "invalidState": 0, - // "fileUuid": "3051020100043630340201000204169df3d602037a1afd020440f165b4020465f02cb304108950eb327e26c01e69d4a0fab7e2b15802037a1db902010004140000000866696c65747970650000000431303031", - "fileSubId": "", - "fileBizId": null, - "originVideoMd5": "", - "import_rich_media_context": null, - "sourceVideoCodecFormat": 0 + // fileUuid: "", + // transferStatus: 0, + // progress: 0, + // invalidState: 0, + // fileSubId: "", + // fileBizId: null, + // originVideoMd5: "", + // fileFormat: 2, + // import_rich_media_context: null, + // sourceVideoCodecFormat: 2 } } return element; - log("video element", element) - let e = { - "elementType": 5, - "elementId": "", - "videoElement": { - "filePath": "E:\\SystemDocuments\\QQ\\721011692\\nt_qq\\nt_data\\Video\\2024-03\\Ori\\8950eb327e26c01e69d4a0fab7e2b158.mp4", - "fileName": "8950eb327e26c01e69d4a0fab7e2b158.mp4", - "videoMd5": "8950eb327e26c01e69d4a0fab7e2b158", - "thumbMd5": "9eee9e9a07b193cbaf4846522b0197b4", - "fileTime": 15, - "thumbSize": 368286, - "fileFormat": 2, - "fileSize": "2084867", - "thumbWidth": 540, - "thumbHeight": 960, - "busiType": 0, - "subBusiType": 0, - "thumbPath": thumbPath, - "transferStatus": 0, - "progress": 0, - "invalidState": 0, - "fileUuid": "3051020100043630340201000204169df3d602037a1afd020440f165b4020465f02cb304108950eb327e26c01e69d4a0fab7e2b15802037a1db902010004140000000866696c65747970650000000431303031", - "fileSubId": "", - "fileBizId": null, - "originVideoMd5": "", - "import_rich_media_context": null, - "sourceVideoCodecFormat": 0 - } - } - return e as SendVideoElement } static async ptt(pttPath: string): Promise { @@ -185,8 +182,7 @@ export class SendMsgElementConstructor { throw "文件异常,大小为0"; } if (converted) { - fs.unlink(silkPath, () => { - }); + fs.unlink(silkPath).then(); } return { elementType: ElementType.PTT, diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index ee2cc57..ec5738f 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -28,6 +28,7 @@ export enum ReceiveCmd { FRIEND_REQUEST = "nodeIKernelBuddyListener/onBuddyReqChange", SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged', CACHE_SCAN_FINISH = "nodeIKernelStorageCleanListener/onFinishScan", + MEDIA_UPLOAD_COMPLETE = "nodeIKernelMsgListener/onRichMediaUploadComplete", } interface NTQQApiReturnData extends Array { diff --git a/src/ntqqapi/types.ts b/src/ntqqapi/types.ts index e7aea8a..6b5e46f 100644 --- a/src/ntqqapi/types.ts +++ b/src/ntqqapi/types.ts @@ -283,7 +283,7 @@ export interface VideoElement { "fileBizId"?: null, "originVideoMd5"?: "", "import_rich_media_context"?: null, - "sourceVideoCodecFormat"?: 0 + "sourceVideoCodecFormat"?: number } export interface TipAioOpGrayTipElement { // 这是什么提示来着? diff --git a/src/onebot11/action/BaseAction.ts b/src/onebot11/action/BaseAction.ts index 09b3752..05ab409 100644 --- a/src/onebot11/action/BaseAction.ts +++ b/src/onebot11/action/BaseAction.ts @@ -22,7 +22,7 @@ class BaseAction { return OB11Response.ok(resData); } catch (e) { log("发生错误", e) - return OB11Response.error(e.toString(), 200); + return OB11Response.error(e?.toString() || e?.stack?.toString() || "未知错误,可能操作超时", 200); } } @@ -36,7 +36,7 @@ class BaseAction { return OB11Response.ok(resData, echo); } catch (e) { log("发生错误", e) - return OB11Response.error(e.toString(), 1200, echo) + return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo) } } diff --git a/src/onebot11/action/SendMsg.ts b/src/onebot11/action/SendMsg.ts index 366780b..d58f0ac 100644 --- a/src/onebot11/action/SendMsg.ts +++ b/src/onebot11/action/SendMsg.ts @@ -157,14 +157,9 @@ export class SendMsg extends BaseAction { } // log("send msg:", peer, sendElements) const {sendElements, deleteAfterSentFiles} = await this.createSendElements(messages, group) - try { const returnMsg = await this.send(peer, sendElements, deleteAfterSentFiles) deleteAfterSentFiles.map(f => fs.unlink(f, () => {})); return {message_id: returnMsg.msgShortId} - } catch (e) { - log("发送消息失败", e.stack.toString()) - throw (e.toString()) - } } protected convertMessage2List(message: OB11MessageMixType) {