import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' import { getS3ObjectStream, isS3Url } from '@/lib/s3' import { findValidMaterialShareToken } from '@/lib/material-share-token' import { createLocalFileStreamResponse } from '@/lib/local-file-stream' import { isPublicShareEnabled } from '@/lib/public-share-settings' import { access } from 'fs/promises' import path from 'path' import { Readable } from 'stream' export const dynamic = 'force-dynamic' // 视频/音频流式传输 API:内部播放需登录,公开分享需有效分享票据。 export async function GET(req: NextRequest) { try { const { searchParams } = new URL(req.url) const id = searchParams.get('id') const shareToken = searchParams.get('shareToken') || searchParams.get('token') || '' if (!id) { return NextResponse.json( { error: '缺少素材ID' }, { status: 400 } ) } const material = await prisma.material.findFirst({ where: { id, deletedAt: null, }, select: { id: true, zipFilePath: true, zipFileName: true, materialType: true, materialKind: true, }, }) if (!material) { return NextResponse.json( { error: '素材不存在' }, { status: 404 } ) } const session = await getServerSession(authOptions) if (!session?.user) { if (!(await isPublicShareEnabled())) { return NextResponse.json( { error: '公开分享功能已关闭' }, { status: 403 } ) } const validShareToken = shareToken ? await findValidMaterialShareToken(shareToken, material.id) : null if (!validShareToken) { return NextResponse.json( { error: '请先登录或使用有效分享链接' }, { status: 401 } ) } } // 只允许视频和音频类型 const isVideo = material.materialType === '视频' || material.materialKind === 'VIDEO' const isAudio = material.materialType === '音频' || material.materialKind === 'AUDIO' if (!isVideo && !isAudio) { return NextResponse.json( { error: '此API仅支持视频和音频类型素材' }, { status: 403 } ) } if (!material.zipFilePath) { return NextResponse.json( { error: '未找到素材文件,请重新上传' }, { status: 404 } ) } // 根据文件扩展名确定Content-Type const originalExt = path.extname(material.zipFileName || material.zipFilePath || '').toLowerCase() const videoTypes: { [key: string]: string } = { '.mp4': 'video/mp4', '.avi': 'video/x-msvideo', '.mov': 'video/quicktime', '.wmv': 'video/x-ms-wmv', '.flv': 'video/x-flv', '.mkv': 'video/x-matroska', '.webm': 'video/webm', } const audioTypes: { [key: string]: string } = { '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.flac': 'audio/flac', '.aac': 'audio/aac', '.ogg': 'audio/ogg', '.m4a': 'audio/mp4', '.wma': 'audio/x-ms-wma', } const defaultType = isAudio ? 'audio/mpeg' : 'video/mp4' const contentType = videoTypes[originalExt] || audioTypes[originalExt] || defaultType const range = req.headers.get('range') if (isS3Url(material.zipFilePath)) { const s3Object = await getS3ObjectStream(material.zipFilePath, range) const bodyStream = toWebStream(s3Object.body) const headers: Record = { 'Content-Type': s3Object.contentType || contentType, 'Accept-Ranges': s3Object.acceptRanges || 'bytes', 'Cache-Control': 'private, max-age=300', } if (range && s3Object.contentRange) { headers['Content-Range'] = s3Object.contentRange } if (!range && s3Object.contentLength) { headers['Content-Length'] = s3Object.contentLength.toString() } const status = range ? 206 : 200 return new NextResponse(bodyStream as any, { status, headers, }) } // Legacy local file support let filePath = material.zipFilePath if (!path.isAbsolute(filePath)) { filePath = path.join(process.cwd(), filePath) } console.log('Streaming legacy video file:', filePath) try { await access(filePath) } catch (error) { console.error('File not found:', filePath) return NextResponse.json( { error: '文件不存在,可能已被删除' }, { status: 404 } ) } return createLocalFileStreamResponse(filePath, { contentType, range, headers: { 'Cache-Control': 'private, max-age=300', }, }) } catch (error) { console.error('Stream error:', error) return NextResponse.json( { error: '流式传输失败: ' + (error instanceof Error ? error.message : '未知错误') }, { status: 500 } ) } } function toWebStream(body: any) { if (!body) { throw new Error('S3 object body is empty') } if (typeof (body as any).transformToWebStream === 'function') { return (body as any).transformToWebStream() } if (body instanceof Readable) { return Readable.toWeb(body) } return body as ReadableStream }