245 lines
6.4 KiB
TypeScript
245 lines
6.4 KiB
TypeScript
/**
|
|
* 视频转码 API
|
|
*
|
|
* 处理视频素材的转码任务:
|
|
* - 生成 540P 预览视频
|
|
* - 生成 10 秒超小预览片段
|
|
* - 从视频中截取封面图(如果用户没有上传)
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import { getServerSession } from 'next-auth'
|
|
import { authOptions } from '@/lib/auth'
|
|
import { isCurrentSuperAdmin } from '@/lib/admin-permissions'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { enqueueVideoTranscodeJob, recoverStaleVideoTranscodeJobs } from '@/lib/video-transcode-service'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
export const maxDuration = 3600 // 1 hour timeout for transcode
|
|
|
|
function getSessionUser(session: unknown) {
|
|
return (session as any)?.user as { id?: string; role?: string } | undefined
|
|
}
|
|
|
|
function canManageTranscode(session: unknown, material: { userId?: string | null; createdByUserId?: string | null }) {
|
|
const user = getSessionUser(session)
|
|
if (!user?.id) return false
|
|
if (user.role === 'ADMIN') return true
|
|
return material.userId === user.id || material.createdByUserId === user.id
|
|
}
|
|
|
|
/**
|
|
* GET /api/materials/transcode?materialId=xxx
|
|
* 查询视频转码状态
|
|
*/
|
|
export async function GET(req: NextRequest) {
|
|
try {
|
|
const session = await getServerSession(authOptions)
|
|
if (!getSessionUser(session)?.id) {
|
|
return NextResponse.json(
|
|
{ error: '请先登录' },
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
const { searchParams } = new URL(req.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,
|
|
userId: true,
|
|
createdByUserId: true,
|
|
videoProcessingStatus: true,
|
|
videoPreview540Url: true,
|
|
videoPreviewClipUrl: true,
|
|
videoCoverUrl: true,
|
|
videoDuration: true,
|
|
},
|
|
})
|
|
|
|
if (!material) {
|
|
return NextResponse.json(
|
|
{ error: '素材不存在' },
|
|
{ status: 404 }
|
|
)
|
|
}
|
|
|
|
if (!canManageTranscode(session, material)) {
|
|
return NextResponse.json(
|
|
{ error: '无权查看该素材的转码状态' },
|
|
{ status: 403 }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json({
|
|
materialId: material.id,
|
|
status: material.videoProcessingStatus || 'pending',
|
|
hasPreview540: !!material.videoPreview540Url,
|
|
hasClip: !!material.videoPreviewClipUrl,
|
|
hasCover: !!material.videoCoverUrl,
|
|
duration: material.videoDuration,
|
|
})
|
|
} catch (error) {
|
|
console.error('[Transcode] GET error:', error)
|
|
return NextResponse.json(
|
|
{ error: '查询转码状态失败' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/materials/transcode
|
|
* 触发视频转码任务
|
|
*/
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const session = await getServerSession(authOptions)
|
|
if (!getSessionUser(session)?.id) {
|
|
return NextResponse.json(
|
|
{ error: '请先登录' },
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
const body = await req.json().catch(() => ({}))
|
|
const { materialId } = body
|
|
|
|
if (!materialId) {
|
|
return NextResponse.json(
|
|
{ error: '缺少素材ID' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
// 获取素材信息
|
|
const material = await prisma.material.findUnique({
|
|
where: { id: materialId },
|
|
select: {
|
|
id: true,
|
|
userId: true,
|
|
createdByUserId: true,
|
|
materialKind: true,
|
|
videoSourceUrl: true,
|
|
videoCoverUrl: true,
|
|
videoProcessingStatus: true,
|
|
materialType: true,
|
|
},
|
|
})
|
|
|
|
if (!material) {
|
|
return NextResponse.json(
|
|
{ error: '素材不存在' },
|
|
{ status: 404 }
|
|
)
|
|
}
|
|
|
|
if (!canManageTranscode(session, material)) {
|
|
return NextResponse.json(
|
|
{ error: '无权触发该素材的转码任务' },
|
|
{ status: 403 }
|
|
)
|
|
}
|
|
|
|
// 检查是否为视频素材
|
|
const materialKind = (material as any).materialKind || ''
|
|
const isVideo = materialKind.toUpperCase() === 'VIDEO' || material.materialType === '视频'
|
|
if (!isVideo) {
|
|
return NextResponse.json(
|
|
{ error: '此素材不是视频类型' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
// 检查是否已有源视频
|
|
if (!material.videoSourceUrl) {
|
|
return NextResponse.json(
|
|
{ error: '未找到源视频文件' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
// 检查转码状态
|
|
const forceRetry = body.forceRetry === true // 允许强制重新转码
|
|
|
|
if (material.videoProcessingStatus === 'processing' && !forceRetry) {
|
|
return NextResponse.json(
|
|
{ error: '转码任务正在进行中', materialId },
|
|
{ status: 409 }
|
|
)
|
|
}
|
|
|
|
if (material.videoProcessingStatus === 'done' && !forceRetry) {
|
|
return NextResponse.json(
|
|
{ message: '转码已完成', materialId },
|
|
{ status: 200 }
|
|
)
|
|
}
|
|
|
|
// 如果强制重试,重置状态
|
|
if (forceRetry && material.videoProcessingStatus === 'done') {
|
|
console.log(`[Transcode] Force retry requested for material ${materialId}`)
|
|
}
|
|
|
|
await enqueueVideoTranscodeJob({
|
|
materialId,
|
|
videoSourceUrl: material.videoSourceUrl,
|
|
needsCoverImage: !material.videoCoverUrl,
|
|
forceRetry,
|
|
})
|
|
|
|
return NextResponse.json({
|
|
message: '转码任务已启动',
|
|
materialId,
|
|
status: 'pending',
|
|
})
|
|
} catch (error) {
|
|
console.error('[Transcode] API error:', error)
|
|
return NextResponse.json(
|
|
{ error: '转码任务启动失败' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
|
|
export async function PATCH(req: NextRequest) {
|
|
try {
|
|
const session = await getServerSession(authOptions)
|
|
if (!(await isCurrentSuperAdmin(session))) {
|
|
return NextResponse.json(
|
|
{ error: '无权限' },
|
|
{ status: 403 }
|
|
)
|
|
}
|
|
|
|
const body = await req.json().catch(() => ({}))
|
|
if (body.action !== 'recover') {
|
|
return NextResponse.json(
|
|
{ error: '不支持的操作' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
const result = await recoverStaleVideoTranscodeJobs()
|
|
return NextResponse.json({
|
|
message: '已触发转码任务恢复',
|
|
...result,
|
|
})
|
|
} catch (error) {
|
|
console.error('[Transcode] Recovery error:', error)
|
|
return NextResponse.json(
|
|
{ error: '转码任务恢复失败' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|