gongxiangp2/docs/视频转码问题修复说明.md

254 lines
7.3 KiB
Markdown
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.

# 视频转码问题修复说明
## 问题描述
视频类素材上传后,后台转码任务未触发。素材 ID: `0895f84d-8837-465e-be50-4db57b5eab9e`
## 根本原因分析
通过诊断脚本发现以下问题:
1. **类型不匹配问题**
- `lib/video-transcode-queue.ts` 的 `process` 方法中,`taskFn` 的类型定义缺少 `retryCount` 参数
- 导致 TypeScript 类型检查可能失败,或运行时参数传递错误
2. **错误被静默吞掉**
- `app/api/materials/upload/route.ts` 中,转码任务入队失败时,错误被 catch 捕获但只打印日志
- 没有更新数据库状态,导致无法追踪问题
3. **动态导入可能失败**
- 使用 `await import('@/lib/video-transcode-task')` 可能因模块路径、循环依赖等问题失败
- 错误被捕获后,用户无法感知
4. **缺少文件验证**
- 入队前未验证视频文件是否存在
- 如果文件不存在,转码会失败,但错误信息不够明确
5. **队列处理逻辑不完善**
- 队列处理时缺少详细的日志和错误处理
- 如果 `process` 方法本身出错,可能导致队列卡死
6. **恢复逻辑只在 POST 请求时触发**
- 恢复逻辑只在转码 API 的 POST 方法中触发
- GET 请求(前端轮询)时不会触发恢复,导致 pending 状态的视频无法自动恢复
## 修复内容
### 1. 修复类型不匹配 (`lib/video-transcode-queue.ts`)
**问题**:`process` 方法的 `taskFn` 类型定义与 `add` 方法不一致
**修复**:
```typescript
// 修复前
private async process(
taskFn: (materialId: string, videoSourceUrl: string, needsCoverImage: boolean) => Promise<void>
): Promise<void>
// 修复后
private async process(
taskFn: (materialId: string, videoSourceUrl: string, needsCoverImage: boolean, retryCount?: number) => Promise<void>
): Promise<void>
```
### 2. 改进错误处理和日志 (`app/api/materials/upload/route.ts`)
**问题**:入队失败时错误被静默吞掉
**修复**:
- 改用静态导入,避免动态导入失败
- 入队失败时,记录详细错误信息到数据库
- 保持 `pending` 状态,允许手动重试
- 添加详细的日志输出
```typescript
// 修复后
import { enqueueVideoTranscodeTask } from '@/lib/video-transcode-task';
if (videoSourceUrl) {
const needsCoverImage = !previewUrl || previewUrl.trim() === '';
try {
await enqueueVideoTranscodeTask(material.id, videoSourceUrl, needsCoverImage);
console.log('[Upload] ✅ Video transcode task enqueued successfully:', material.id);
} catch (e: any) {
// 记录详细错误信息到数据库
const errorMsg = e?.message || String(e) || '未知错误';
const errorStack = e?.stack ? `\n堆栈: ${e.stack.substring(0, 1000)}` : '';
const fullErrorMsg = `入队失败: ${errorMsg}${errorStack}`;
console.error('[Upload] ❌ Failed to enqueue video transcode task:', {
materialId: material.id,
videoSourceUrl,
error: errorMsg,
stack: e?.stack,
});
// 更新数据库状态,记录错误信息
await prisma.material.update({
where: { id: material.id },
data: {
videoProcessingStatus: 'pending',
transcodeLastError: fullErrorMsg.substring(0, 1000),
} as any,
});
}
}
```
### 3. 添加文件验证 (`lib/video-transcode-task.ts`)
**问题**:入队前未验证文件是否存在
**修复**:
- 添加 `validateVideoFile` 函数,验证视频文件是否存在
- 入队前先验证文件,如果不存在则抛出错误
```typescript
async function validateVideoFile(videoSourceUrl: string): Promise<boolean> {
// 验证逻辑...
}
export async function enqueueVideoTranscodeTask(...) {
// 验证视频文件是否存在
const fileExists = await validateVideoFile(videoSourceUrl)
if (!fileExists) {
throw new Error(`视频文件不存在: ${videoSourceUrl}`)
}
// 入队...
}
```
### 4. 改进队列处理逻辑 (`lib/video-transcode-queue.ts`)
**问题**:队列处理时缺少详细的日志和错误处理
**修复**:
- 添加详细的日志输出
- 改进错误处理,确保队列不会卡死
- 添加任务参数验证
- 改进 `add` 方法,检查重复任务
```typescript
async add(...) {
// 检查是否已在队列中等待
const alreadyInQueue = this.queue.some(t => t.materialId === materialId)
if (alreadyInQueue) {
console.log(`[TranscodeQueue] Material ${materialId} is already in queue, skipping`)
return
}
console.log(`[TranscodeQueue] Adding task to queue: materialId=${materialId}...`)
// ...
}
private async process(...) {
try {
// 验证任务参数
if (!task.materialId || !task.videoSourceUrl) {
console.error(`[TranscodeQueue] Invalid task parameters:`, task)
task.reject(new Error('Invalid task parameters'))
return
}
// 详细的日志输出
console.log(`[TranscodeQueue] 🚀 Starting task for material ${task.materialId}...`)
// 改进的错误处理
// ...
} catch (error) {
console.error('[TranscodeQueue] Error in process() method:', error)
// 确保队列不会卡死
setTimeout(() => {
this.process(taskFn).catch(err => {
console.error('[TranscodeQueue] Error in retry process():', err)
})
}, 1000)
}
}
```
### 5. 改进恢复逻辑 (`app/api/materials/transcode/route.ts`)
**问题**:恢复逻辑只在 POST 请求时触发
**修复**:
- 在 GET 请求时也触发恢复逻辑
- 确保前端轮询时能自动恢复 pending 状态的视频
```typescript
// GET - 查询转码状态
export async function GET(request: NextRequest) {
// 懒加载恢复:第一次调用时自动恢复中断的任务(包括 GET 请求)
await ensureRecoveryCompleted();
// ...
}
```
## 新增工具
### 1. 诊断脚本 (`scripts/diagnose-video-transcode.ts`)
用于诊断视频转码问题,检查:
- 素材信息
- 视频文件是否存在
- 转码状态分析
- 转码产物检查
- 诊断总结和建议
**使用方法**:
```bash
tsx scripts/diagnose-video-transcode.ts <materialId>
```
### 2. 手动触发转码脚本 (`scripts/manual-trigger-transcode.ts`)
用于手动触发视频转码,修复未触发转码的素材。
**使用方法**:
```bash
tsx scripts/manual-trigger-transcode.ts <materialId>
```
## 验证修复
1. **检查修复后的代码**
- 所有类型定义已统一
- 错误处理已完善
- 日志输出已详细
2. **测试新上传的视频**
- 上传一个视频素材
- 检查是否自动触发转码
- 查看日志确认入队成功
3. **修复已有问题素材**
- 使用诊断脚本检查问题素材
- 使用手动触发脚本修复
## 后续建议
1. **监控和告警**
- 添加转码任务入队失败的监控
- 设置告警,及时发现未触发的转码任务
2. **定期检查**
- 定期检查 pending 状态的视频
- 自动触发未转码的视频
3. **改进上传逻辑**
- 确保所有视频类型都正确设置 `videoSourceUrl` 为本地路径
- 对于旧版本的 S3 URL,转码时自动处理
## 相关文件
- `app/api/materials/upload/route.ts` - 上传接口
- `lib/video-transcode-task.ts` - 转码任务封装
- `lib/video-transcode-queue.ts` - 转码队列
- `app/api/materials/transcode/route.ts` - 转码 API
- `scripts/diagnose-video-transcode.ts` - 诊断脚本
- `scripts/manual-trigger-transcode.ts` - 手动触发脚本