// 视频转码API - 完整版本(支持自动恢复) import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; import { transcodeQueue, cleanupTranscodeResidues } from '@/lib/video-transcode-queue'; import { executeTranscodeTask, enqueueVideoTranscodeTask } from '@/lib/video-transcode-task'; export const dynamic = 'force-dynamic'; export const maxDuration = 3600; // 1小时 // 恢复标志:确保只执行一次 let recoveryChecked = false; /** * 懒加载恢复中断的转码任务 */ async function ensureRecoveryCompleted(): Promise { if (recoveryChecked) { return; } recoveryChecked = true; try { console.log('[Transcode] Starting recovery check...'); const { recovered, requeued } = await transcodeQueue.recoverInterruptedTasks( executeTranscodeTask, cleanupTranscodeResidues ); console.log(`[Transcode] Recovery completed: ${recovered} tasks recovered, ${requeued} tasks requeued`); } catch (error) { console.error('[Transcode] Recovery failed:', error); // 恢复失败不影响正常流程,重置标志以便下次重试 recoveryChecked = false; } } // GET - 查询转码状态 export async function GET(request: NextRequest) { // 懒加载恢复:第一次调用时自动恢复中断的任务(包括 GET 请求) await ensureRecoveryCompleted(); try { const { searchParams } = new URL(request.url); const materialId = searchParams.get('materialId'); if (!materialId) { return NextResponse.json( { error: '缺少素材ID' }, { status: 400 } ); } const material = await prisma.material.findUnique({ where: { id: materialId }, select: { id: true, videoProcessingStatus: true, videoPreview540Url: true, videoPreviewClipUrl: true, videoCoverUrl: true, videoDuration: true, transcodeLastError: true, transcodeAttempts: true, } as any, }); if (!material) { return NextResponse.json( { error: '素材不存在' }, { status: 404 } ); } // 获取转码队列状态 const queueStatus = transcodeQueue.getStatus(); const isProcessing = queueStatus.processingIds.includes(materialId); const waitingIndex = queueStatus.waiting.findIndex(id => id === materialId); const waitingTotal = queueStatus.waiting.length; return NextResponse.json({ materialId: material.id, status: material.videoProcessingStatus || 'pending', hasPreview540: !!material.videoPreview540Url, hasClip: !!material.videoPreviewClipUrl, hasCover: !!material.videoCoverUrl, duration: material.videoDuration, attempts: material.transcodeAttempts || 0, lastError: material.transcodeLastError, progress: isProcessing ? queueStatus.progress.get(materialId) || null : null, stage: isProcessing ? queueStatus.stages.get(materialId) || null : null, queue: { waitingIndex: waitingIndex >= 0 ? waitingIndex + 1 : null, waitingTotal: waitingTotal, processingIds: queueStatus.processingIds, maxConcurrent: queueStatus.maxConcurrent, }, }); } catch (error) { console.error('[Transcode] GET error:', error); return NextResponse.json( { error: '查询转码状态失败' }, { status: 500 } ); } } // POST - 启动转码任务 export async function POST(request: NextRequest) { // 懒加载恢复:第一次调用时自动恢复中断的任务 await ensureRecoveryCompleted(); try { const { materialId, forceRetry } = await request.json(); if (!materialId) { return NextResponse.json( { error: '缺少素材ID' }, { status: 400 } ); } const material = await prisma.material.findFirst({ where: { id: materialId, deletedAt: null }, select: { id: true, videoSourceUrl: true, videoCoverUrl: true, videoProcessingStatus: true, materialType: true, } as any, }); if (!material) { return NextResponse.json( { error: '素材不存在或已被删除' }, { status: 404 } ); } // 检查是否为视频类型 const videoTypes = ['视频', 'VIDEO', '主图视频', '安装视频', '种草视频']; if (!videoTypes.includes((material as any).materialType)) { return NextResponse.json( { error: '此素材不是视频类型' }, { status: 400 } ); } // 检查是否有源视频 if (!(material as any).videoSourceUrl) { return NextResponse.json( { error: '未找到源视频文件' }, { status: 400 } ); } // 检查转码状态 if ((material as any).videoProcessingStatus === 'processing' && !forceRetry) { return NextResponse.json( { error: '转码任务正在进行中', materialId }, { status: 409 } ); } if ((material as any).videoProcessingStatus === 'done' && !forceRetry) { return NextResponse.json( { message: '转码已完成', materialId }, { status: 200 } ); } // 判断是否需要生成封面图(用户未上传时才生成) const needsCoverImage = !(material as any).videoCoverUrl; // 加入转码队列 await transcodeQueue.add( materialId, (material as any).videoSourceUrl, needsCoverImage, executeTranscodeTask ); console.log(`[Transcode] Task queued for material ${materialId}`); return NextResponse.json({ message: '转码任务已启动', materialId, status: 'processing', queueStatus: transcodeQueue.getStatus(), }); } catch (error) { console.error('[Transcode] POST error:', error); return NextResponse.json( { error: '转码任务启动失败' }, { status: 500 } ); } }