Some checks failed
CI Check / check (push) Has been cancelled
- 移除高亮卡片效果,避免与虚位卡片视觉重复 - 松开鼠标后立即更新布局,不再等待动画完成 - 优化拖拽动画效果,使用弹性缓动函数 - 更新 CHANGELOG.md 记录本次优化
114 lines
3.2 KiB
TypeScript
114 lines
3.2 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
||
import { prisma } from '@/lib/prisma'
|
||
import { readFile, access } from 'fs/promises'
|
||
import path from 'path'
|
||
|
||
export const dynamic = 'force-dynamic'
|
||
|
||
// 视频流式传输API(允许未登录用户访问,仅用于视频播放)
|
||
export async function GET(req: NextRequest) {
|
||
try {
|
||
const { searchParams } = new URL(req.url)
|
||
const id = searchParams.get('id')
|
||
|
||
if (!id) {
|
||
return NextResponse.json(
|
||
{ error: '缺少素材ID' },
|
||
{ status: 400 }
|
||
)
|
||
}
|
||
|
||
const material = await prisma.material.findUnique({
|
||
where: { id },
|
||
})
|
||
|
||
if (!material) {
|
||
return NextResponse.json(
|
||
{ error: '素材不存在' },
|
||
{ status: 404 }
|
||
)
|
||
}
|
||
|
||
// 只允许视频类型
|
||
if (material.materialType !== '视频') {
|
||
return NextResponse.json(
|
||
{ error: '此API仅支持视频类型素材' },
|
||
{ status: 403 }
|
||
)
|
||
}
|
||
|
||
// Read file from local storage
|
||
let filePath = material.zipFilePath
|
||
if (!path.isAbsolute(filePath)) {
|
||
filePath = path.join(process.cwd(), filePath)
|
||
}
|
||
|
||
console.log('Streaming video file:', filePath)
|
||
|
||
// 检查文件是否存在
|
||
try {
|
||
await access(filePath)
|
||
} catch (error) {
|
||
console.error('File not found:', filePath)
|
||
return NextResponse.json(
|
||
{ error: '文件不存在,可能已被删除' },
|
||
{ status: 404 }
|
||
)
|
||
}
|
||
|
||
const fileBuffer = await readFile(filePath)
|
||
|
||
// 根据文件扩展名确定Content-Type
|
||
const originalExt = path.extname(material.zipFileName).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 contentType = videoTypes[originalExt] || 'video/mp4'
|
||
|
||
// 支持Range请求(视频流式传输)
|
||
const range = req.headers.get('range')
|
||
if (range) {
|
||
const fileSize = fileBuffer.length
|
||
const parts = range.replace(/bytes=/, '').split('-')
|
||
const start = parseInt(parts[0], 10)
|
||
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1
|
||
const chunkSize = end - start + 1
|
||
const chunk = fileBuffer.slice(start, end + 1)
|
||
|
||
return new NextResponse(chunk as any, {
|
||
status: 206,
|
||
headers: {
|
||
'Content-Type': contentType,
|
||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||
'Accept-Ranges': 'bytes',
|
||
'Content-Length': chunkSize.toString(),
|
||
'Cache-Control': 'public, max-age=3600', // 缓存1小时
|
||
},
|
||
})
|
||
}
|
||
|
||
// 非Range请求,返回完整文件
|
||
return new NextResponse(fileBuffer as any, {
|
||
headers: {
|
||
'Content-Type': contentType,
|
||
'Content-Length': fileBuffer.length.toString(),
|
||
'Accept-Ranges': 'bytes',
|
||
'Cache-Control': 'public, max-age=3600',
|
||
},
|
||
})
|
||
} catch (error) {
|
||
console.error('Stream error:', error)
|
||
return NextResponse.json(
|
||
{ error: '流式传输失败: ' + (error instanceof Error ? error.message : '未知错误') },
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|
||
|