254 lines
7.3 KiB
Markdown
254 lines
7.3 KiB
Markdown
# 视频转码问题修复说明
|
||
|
||
## 问题描述
|
||
|
||
视频类素材上传后,后台转码任务未触发。素材 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` - 手动触发脚本
|
||
|