199 lines
5.8 KiB
TypeScript
199 lines
5.8 KiB
TypeScript
// 视频转码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<void> {
|
|
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.findUnique({
|
|
where: { id: materialId },
|
|
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 }
|
|
);
|
|
}
|
|
}
|