feat: task scheduler; feat: ffmpeg compress webp to avif
This commit is contained in:
@@ -3,6 +3,10 @@ import { prisma } from '@/lib/prisma'
|
||||
import { storeFile } from '@/lib/fileStorage'
|
||||
import { push } from '@/lib/push'
|
||||
import { withCors } from '@/lib/middleware'
|
||||
import ffmpeg from 'fluent-ffmpeg'
|
||||
import { writeFileSync, unlinkSync, mkdtempSync, readFileSync, rmSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
interface WindowInfo {
|
||||
title: string
|
||||
@@ -17,7 +21,7 @@ async function handleScreenshotUpload(req: NextRequest) {
|
||||
const pathSegments = req.nextUrl.pathname.split('/')
|
||||
const hostnameIndex = pathSegments.indexOf('hosts') + 1
|
||||
const hostname = pathSegments[hostnameIndex]
|
||||
|
||||
|
||||
if (!hostname) {
|
||||
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
|
||||
}
|
||||
@@ -25,7 +29,7 @@ async function handleScreenshotUpload(req: NextRequest) {
|
||||
const formData = await req.formData()
|
||||
const files: File[] = []
|
||||
const windowsInfo: WindowInfo[] = JSON.parse(formData.get('windows_info') as string || '[]')
|
||||
|
||||
|
||||
// Extract files from formData
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (key.startsWith('screenshot_') && value instanceof File) {
|
||||
@@ -38,23 +42,78 @@ async function handleScreenshotUpload(req: NextRequest) {
|
||||
}
|
||||
|
||||
// Process screenshots
|
||||
const processingStartTime = Date.now()
|
||||
console.log(`[${hostname}] 开始处理 ${files.length} 张截图`)
|
||||
|
||||
const screenshots = await Promise.all(files.map(async (file, index) => {
|
||||
const ext = file.name ? file.name.split('.').pop() : 'webp'
|
||||
const filename = `${hostname}-${Date.now()}-${Math.round(Math.random() * 1E9)}.${ext}`
|
||||
|
||||
const imageStartTime = Date.now()
|
||||
|
||||
// Convert image to AV1 format using FFmpeg
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
const storedFile = await storeFile(buffer, filename, file.type, 'screenshot', hostname)
|
||||
|
||||
return {
|
||||
fileId: storedFile.id,
|
||||
objectName: storedFile.objectName,
|
||||
filename: storedFile.filename,
|
||||
contentType: storedFile.contentType,
|
||||
fileSize: storedFile.size,
|
||||
monitorName: formData.get(`monitor_name_${index}`) as string || `Monitor ${index + 1}`
|
||||
const originalSize = buffer.length
|
||||
|
||||
// Create temporary files
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'ffmpeg-'))
|
||||
const inputPath = join(tempDir, `input_${index}.png`)
|
||||
const outputPath = join(tempDir, `output_${index}.avif`)
|
||||
|
||||
try {
|
||||
// Write input file
|
||||
writeFileSync(inputPath, buffer)
|
||||
|
||||
const conversionStartTime = Date.now()
|
||||
|
||||
// Convert using FFmpeg with libsvtav1
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
ffmpeg(inputPath)
|
||||
.videoCodec('libsvtav1')
|
||||
.outputOptions([
|
||||
'-crf 30', // 质量控制,值越小质量越好
|
||||
'-preset 6', // 编码速度预设,0-12,值越大速度越快但质量可能下降
|
||||
'-pix_fmt yuv420p' // 像素格式
|
||||
])
|
||||
.output(outputPath)
|
||||
.on('end', () => resolve())
|
||||
.on('error', (err) => reject(err))
|
||||
.run()
|
||||
})
|
||||
|
||||
const conversionTime = Date.now() - conversionStartTime
|
||||
|
||||
// Read the converted file
|
||||
const avifBuffer = readFileSync(outputPath)
|
||||
const filename = `${hostname}-${Date.now()}-${Math.round(Math.random() * 1E9)}.avif`
|
||||
|
||||
const storageStartTime = Date.now()
|
||||
const storedFile = await storeFile(avifBuffer, filename, 'image/avif', 'screenshot', hostname)
|
||||
const storageTime = Date.now() - storageStartTime
|
||||
|
||||
const totalImageTime = Date.now() - imageStartTime
|
||||
const compressionRatio = ((originalSize - avifBuffer.length) / originalSize * 100).toFixed(2)
|
||||
|
||||
console.log(`[${hostname}] 图片 ${index + 1}: FFmpeg转换 ${conversionTime}ms, 存储 ${storageTime}ms, 总计 ${totalImageTime}ms, 压缩率 ${compressionRatio}% (${originalSize} -> ${avifBuffer.length} bytes)`)
|
||||
|
||||
return {
|
||||
fileId: storedFile.id,
|
||||
objectName: storedFile.objectName,
|
||||
filename: storedFile.filename,
|
||||
contentType: storedFile.contentType,
|
||||
fileSize: storedFile.size,
|
||||
monitorName: formData.get(`monitor_name_${index}`) as string || `Monitor ${index + 1}`
|
||||
}
|
||||
} finally {
|
||||
// Clean up temporary files
|
||||
try {
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
} catch (err) {
|
||||
console.warn(`清理临时文件失败: ${err}`)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const totalProcessingTime = Date.now() - processingStartTime
|
||||
console.log(`[${hostname}] 完成处理 ${files.length} 张截图,总耗时 ${totalProcessingTime}ms,平均每张 ${(totalProcessingTime / files.length).toFixed(2)}ms`)
|
||||
|
||||
// Ensure host exists first
|
||||
await prisma.host.upsert({
|
||||
where: { hostname },
|
||||
@@ -124,7 +183,7 @@ async function handleGetScreenshots(req: NextRequest) {
|
||||
const pathSegments = req.nextUrl.pathname.split('/')
|
||||
const hostnameIndex = pathSegments.indexOf('hosts') + 1
|
||||
const hostname = pathSegments[hostnameIndex]
|
||||
|
||||
|
||||
if (!hostname) {
|
||||
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
|
||||
}
|
||||
@@ -132,27 +191,27 @@ async function handleGetScreenshots(req: NextRequest) {
|
||||
const { searchParams } = req.nextUrl
|
||||
const startTimeParam = searchParams.get('startTime')
|
||||
const endTimeParam = searchParams.get('endTime')
|
||||
|
||||
|
||||
let startTime: Date | undefined
|
||||
let endTime: Date | undefined
|
||||
|
||||
|
||||
if (startTimeParam) {
|
||||
const timestamp = isNaN(Number(startTimeParam)) ?
|
||||
new Date(startTimeParam) :
|
||||
const timestamp = isNaN(Number(startTimeParam)) ?
|
||||
new Date(startTimeParam) :
|
||||
new Date(Number(startTimeParam) * 1000)
|
||||
startTime = timestamp
|
||||
}
|
||||
|
||||
|
||||
if (endTimeParam) {
|
||||
const timestamp = isNaN(Number(endTimeParam)) ?
|
||||
new Date(endTimeParam) :
|
||||
const timestamp = isNaN(Number(endTimeParam)) ?
|
||||
new Date(endTimeParam) :
|
||||
new Date(Number(endTimeParam) * 1000)
|
||||
endTime = timestamp
|
||||
}
|
||||
|
||||
// Build query conditions
|
||||
const whereClause: any = { hostname }
|
||||
|
||||
|
||||
if (startTime || endTime) {
|
||||
whereClause.timestamp = {}
|
||||
if (startTime) whereClause.timestamp.gte = startTime
|
||||
@@ -179,7 +238,7 @@ async function handleGetScreenshots(req: NextRequest) {
|
||||
lastUpdate: true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (!host) {
|
||||
return NextResponse.json({ error: '未找到主机记录' }, { status: 404 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user