gongxiangp2/lib/video-transcode-task.ts
chenpei 0ce59be34e 备份:组织架构设计方案讨论前的代码快照
- 包含所有当前修改和新功能
- 准备开始组织架构优化实施方案
2025-12-30 13:20:26 +08:00

170 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 视频转码任务封装,供上传接口和转码API复用
import { prisma } from '@/lib/prisma'
import { transcodeQueue } from '@/lib/video-transcode-queue'
import { processVideoTranscode } from '@/lib/video-processor'
/**
* 实际的转码任务处理函数
*
* 注意:retryCount 由队列控制,外部一般不用直接传
*/
export async function executeTranscodeTask(
materialId: string,
videoSourceUrl: string,
needsCoverImage: boolean,
retryCount: number = 0
): Promise<void> {
const MAX_RETRIES = 3
try {
console.log(`[Transcode] Starting task for material ${materialId} (attempt ${retryCount + 1}/${MAX_RETRIES})`)
// 更新状态为处理中
await prisma.material.update({
where: { id: materialId },
data: {
videoProcessingStatus: 'processing',
transcodeLastError: null,
} as any,
})
// 执行转码
const result = await processVideoTranscode({
inputPath: videoSourceUrl,
outputDir: './uploads/transcoded',
materialId,
needsCoverImage,
onProgress: (progress, stage) => {
console.log(`[Transcode] Material ${materialId} - ${stage}: ${progress}%`)
// 更新转码队列的进度信息
transcodeQueue.updateProgress(materialId, progress, stage)
},
})
// 查询当前素材,避免覆盖用户手动上传的预览图
const currentMaterial = await prisma.material.findUnique({
where: { id: materialId },
select: {
previewUrl: true,
} as any,
})
// 如果用户没有自己上传预览图,而转码生成了封面,则同步一份到 previewUrl
const nextPreviewUrl =
(currentMaterial as any)?.previewUrl && (currentMaterial as any).previewUrl.trim() !== ''
? (currentMaterial as any).previewUrl
: result.coverUrl || null
// 更新数据库
await prisma.material.update({
where: { id: materialId },
data: {
videoProcessingStatus: 'done',
videoPreview540Url: result.preview540Url,
videoPreviewClipUrl: result.clipUrl,
videoCoverUrl: result.coverUrl || undefined,
videoDuration: result.duration,
transcodeLastError: null,
transcodeAttempts: retryCount + 1,
// 同步预览图字段:仅在原来没有 previewUrl 时才使用自动生成的封面
previewUrl: nextPreviewUrl || undefined,
} as any,
})
console.log(`[Transcode] Task completed for material ${materialId}`)
} catch (error: any) {
const errorMsg = error?.message || String(error)
console.error(`[Transcode] Task failed for material ${materialId}:`, errorMsg)
// 如果还有重试次数,重新入队
if (retryCount < MAX_RETRIES - 1) {
console.log(`[Transcode] Retrying material ${materialId} (${retryCount + 1}/${MAX_RETRIES})`)
await transcodeQueue.add(
materialId,
videoSourceUrl,
needsCoverImage,
(id, url, needsCover) => executeTranscodeTask(id, url, needsCover, retryCount + 1)
)
} else {
// 重试次数用尽,标记为失败
await prisma.material.update({
where: { id: materialId },
data: {
videoProcessingStatus: 'failed',
transcodeLastError: errorMsg.substring(0, 500),
transcodeAttempts: retryCount + 1,
} as any,
})
console.error(`[Transcode] Task failed permanently for material ${materialId} after ${MAX_RETRIES} attempts`)
}
}
}
/**
* 验证视频文件是否存在
*/
async function validateVideoFile(videoSourceUrl: string): Promise<boolean> {
try {
// 如果是 HTTP URL,跳过本地文件检查
if (videoSourceUrl.startsWith('http://') || videoSourceUrl.startsWith('https://')) {
return true
}
const path = await import('path')
const { access, constants } = await import('fs/promises')
let localFilePath = videoSourceUrl
// 如果是相对路径(以 /uploads/ 开头),转换为绝对路径
if (videoSourceUrl.startsWith('/uploads/')) {
const uploadDir = process.env.UPLOAD_DIR || './uploads'
const absoluteUploadDir = path.isAbsolute(uploadDir) ? uploadDir : path.join(process.cwd(), uploadDir)
localFilePath = path.join(absoluteUploadDir, videoSourceUrl.replace('/uploads/', ''))
} else if (!path.isAbsolute(videoSourceUrl)) {
// 相对路径:基于当前工作目录
localFilePath = path.join(process.cwd(), videoSourceUrl)
}
// 检查文件是否存在
await access(localFilePath, constants.F_OK)
console.log(`[TranscodeTask] ✅ Video file exists: ${localFilePath}`)
return true
} catch (error) {
console.error(`[TranscodeTask] ❌ Video file not found: ${videoSourceUrl}`, error)
return false
}
}
/**
* 入队一个视频转码任务
*/
export async function enqueueVideoTranscodeTask(
materialId: string,
videoSourceUrl: string,
needsCoverImage: boolean
): Promise<void> {
// 验证视频文件是否存在
const fileExists = await validateVideoFile(videoSourceUrl)
if (!fileExists) {
throw new Error(`视频文件不存在: ${videoSourceUrl}`)
}
console.log(`[TranscodeTask] 📝 Enqueuing transcode task: materialId=${materialId}, videoSourceUrl=${videoSourceUrl}, needsCoverImage=${needsCoverImage}`)
// 注意:这里不等待整个转码流程完成,只负责把任务丢进队列即可
// 队列内部会在任务开始/结束时更新数据库状态和进度
transcodeQueue
.add(materialId, videoSourceUrl, needsCoverImage, executeTranscodeTask)
.then(() => {
console.log(`[TranscodeTask] ✅ Transcode task finished in queue: materialId=${materialId}`)
})
.catch((error) => {
// 这里一般不会抛出未处理错误,因为 executeTranscodeTask 内部已经负责更新失败状态
console.error(`[TranscodeTask] ❌ Transcode task error in queue for material ${materialId}:`, error)
})
console.log(`[TranscodeTask] ✅ Transcode task enqueued successfully (fire-and-forget): materialId=${materialId}`)
}