返回 AiToEarn
volcengine.utils.ts
根目录 / project / aitoearn-backend / apps / aitoearn-ai / src / core / agent / mcp / volcengine / volcengine.utils.ts
1 import { createHash } from 'node:crypto'
2 import { Logger } from '@nestjs/common'
3 import { AssetsService } from '@yikart/assets'
4 import { getErrorMessage } from '@yikart/common'
5 import { AssetType } from '@yikart/mongodb'
6 import sizeOf from 'image-size'
7 import { config } from '../../../../config'
8 import { VolcengineService } from '../../../ai/libs/volcengine'
9
10 /**
11 * 火山引擎视频处理工具类
12 * 提供视频下载、上传、鉴权等通用功能
13 */
14 export class VolcengineVideoUtils {
15 /**
16 * 解析 URL 的各个部分
17 */
18 static splitUrl(url: string): { scheme: string, host: string, path: string, args: string } {
19 const urlObj = new URL(url)
20 return {
21 scheme: `${urlObj.protocol}//`,
22 host: urlObj.host,
23 path: urlObj.pathname,
24 args: urlObj.search || '',
25 }
26 }
27
28 /**
29 * 计算字符串的 MD5 哈希值
30 */
31 static getMd5(text: string): string {
32 return createHash('md5').update(text).digest('hex')
33 }
34
35 /**
36 * 生成指定长度的随机字符串
37 */
38 static getRandomString(length: number): string {
39 const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
40 let result = ''
41 for (let i = 0; i < length; i++) {
42 result += chars[Math.floor(Math.random() * chars.length)]
43 }
44 return result
45 }
46
47 /**
48 * 生成 A 类型鉴权 URL
49 * @param url 基础 URL(完整 URL,如 http://play.vod.com/FileName)
50 * @param key 鉴权密钥
51 * @param ts 时间戳(Unix 时间戳,秒)
52 * @returns 带鉴权参数的完整 URL
53 */
54 static genTypeAUrl(url: string, key: string, ts: number): string {
55 const { scheme, host, path, args } = this.splitUrl(url)
56 const signName = 'auth_key' // 固定为 auth_key
57 const uid = '0' // 固定为 0
58 const randStr = this.getRandomString(10)
59
60 // 计算签名:text = {path}-{ts}-{rand}-{uid}-{key}
61 const text = `${path}-${ts}-${randStr}-${uid}-${key}`
62 const hash = this.getMd5(text)
63
64 // 生成鉴权参数:auth_key={ts}-{rand}-{uid}-{hash}
65 const authArg = `${signName}=${ts}-${randStr}-${uid}-${hash}`
66
67 if (!args) {
68 return `${scheme}${host}${path}?${authArg}`
69 }
70 else {
71 return `${scheme}${host}${path}${args}&${authArg}`
72 }
73 }
74
75 /**
76 * 获取图片尺寸
77 * @param imageUrl 图片 URL
78 * @param logger 日志记录器
79 * @returns 图片的宽度和高度
80 */
81 static async getImageDimensions(
82 imageUrl: string,
83 logger: Logger,
84 ): Promise<{ width: number, height: number }> {
85 try {
86 logger.debug('[getImageDimensions] Fetching image dimensions', { url: imageUrl })
87
88 const response = await fetch(imageUrl)
89 if (!response.ok) {
90 throw new Error(`HTTP ${response.status}`)
91 }
92
93 const arrayBuffer = await response.arrayBuffer()
94 const buffer = Buffer.from(arrayBuffer)
95 const dimensions = sizeOf(buffer)
96
97 if (!dimensions.width || !dimensions.height) {
98 throw new Error('Failed to get image dimensions')
99 }
100
101 logger.debug('[getImageDimensions] Image dimensions retrieved', {
102 width: dimensions.width,
103 height: dimensions.height,
104 })
105
106 return {
107 width: dimensions.width,
108 height: dimensions.height,
109 }
110 }
111 catch (error) {
112 const errorMessage = getErrorMessage(error)
113 logger.error('[getImageDimensions] Failed to get image dimensions', {
114 error: errorMessage,
115 url: imageUrl,
116 })
117 logger.warn('[getImageDimensions] Using default dimensions 1920x1080')
118 return { width: 1920, height: 1080 }
119 }
120 }
121
122 /**
123 * 根据 FileName 拼接播放 URL 并下载上传
124 * @param fileName 文件路径
125 * @param userId 用户 ID
126 * @param filenamePrefix 文件名前缀
127 * @param subPath 子路径(如模型名称)
128 * @param assetsService Assets 服务
129 * @param logger 日志记录器
130 * @param assetType 资源类型
131 * @returns URL 或 undefined
132 */
133 static async saveVideoFromFileName(
134 fileName: string,
135 userId: string,
136 filenamePrefix: string,
137 subPath: string,
138 assetsService: AssetsService,
139 logger: Logger,
140 assetType: AssetType = AssetType.AideoOutput,
141 ): Promise<string | undefined> {
142 const playbackBaseUrl = config.ai.volcengine?.playbackBaseUrl
143 const urlAuthPrimaryKey = config.ai.volcengine?.urlAuthPrimaryKey
144
145 if (!playbackBaseUrl || !urlAuthPrimaryKey) {
146 logger.warn({ fileName }, 'volcengine 未配置,跳过播放 URL 拼接')
147 return undefined
148 }
149
150 const normalizedFileName = fileName.startsWith('/') ? fileName : `/${fileName}`
151 const baseUrl = `${playbackBaseUrl}${normalizedFileName}`
152
153 const ts = Math.floor(Date.now() / 1000) + 3600
154 const playbackUrl = this.genTypeAUrl(baseUrl, urlAuthPrimaryKey, ts)
155
156 logger.debug({ playbackUrl, fileName }, '拼接播放 URL')
157
158 // 使用 fetch 流式下载并上传
159 const response = await fetch(playbackUrl)
160 if (!response.ok || !response.body) {
161 throw new Error(`下载失败: ${response.status}`)
162 }
163
164 const contentLength = response.headers.get('content-length')
165 const size = contentLength ? Number.parseInt(contentLength, 10) : 0
166
167 logger.debug({ size: `${(size / 1024 / 1024).toFixed(2)} MB` }, '开始流式上传')
168
169 const arrayBuffer = await response.arrayBuffer()
170 const buffer = Buffer.from(arrayBuffer)
171
172 const result = await assetsService.uploadFromStream(userId, buffer, {
173 type: assetType,
174 mimeType: 'video/mp4',
175 size,
176 }, subPath)
177
178 return assetsService.buildUrl(result.asset.path)
179 }
180
181 /**
182 * 从 VID 获取视频并上传
183 * @param vid 视频 ID
184 * @param userId 用户 ID
185 * @param filenamePrefix 文件名前缀
186 * @param subPath 子路径(如模型名称)
187 * @param volcengineService 火山引擎服务
188 * @param assetsService Assets 服务
189 * @param logger 日志记录器
190 * @param assetType 资源类型
191 * @returns URL,如果失败则返回 undefined
192 */
193 static async saveVideoFromVid(
194 vid: string,
195 userId: string,
196 filenamePrefix: string,
197 subPath: string,
198 volcengineService: VolcengineService,
199 assetsService: AssetsService,
200 logger: Logger,
201 assetType: AssetType = AssetType.AideoOutput,
202 ): Promise<string | undefined> {
203 const mediaInfo = await volcengineService.getMediaInfos({
204 Vids: vid,
205 })
206
207 const sourceInfo = mediaInfo.MediaInfoList?.[0]?.SourceInfo as Record<string, unknown> | undefined
208
209 const fileName = sourceInfo?.['FileName'] as string
210
211 return this.saveVideoFromFileName(
212 fileName,
213 userId,
214 filenamePrefix,
215 subPath,
216 assetsService,
217 logger,
218 assetType,
219 )
220 }
221 }
222
222 lines TYPESCRIPT