import { randomUUID } from "node:crypto"; import { execFile } from "node:child_process"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; import { uploadFile } from "@/lib/storage/service"; import { downloadCanvasMedia } from "./download-media"; const probe = promisify(execFile); const pending = new Map>(); export async function archiveCanvasVideo(url: string): Promise { const existing = pending.get(url); if (existing) return existing; if (pending.size >= 2) throw new Error("视频保存繁忙,请稍后重试"); const job = (async () => { const dir = await mkdtemp(join(tmpdir(), "canvas-video-archive-")); try { const input = join(dir, "video"); await downloadCanvasMedia(url, input); const { stdout } = await probe("ffprobe", ["-v", "error", "-protocol_whitelist", "file", "-format_whitelist", "mov,matroska,webm", "-show_streams", "-show_format", "-of", "json", input], { timeout: 15000, maxBuffer: 1024 * 1024 }); const media = JSON.parse(stdout); if (!media.streams?.some((stream: { codec_type: string }) => stream.codec_type === "video") || !(Number(media.format?.duration) > 0)) throw new Error("视频文件不完整"); const extension = String(media.format.format_name).includes("mov") ? "mp4" : "webm"; const data = await readFile(input); const uploaded = await uploadFile(`canvas-generated-${randomUUID()}.${extension}`, new Uint8Array(data).buffer); return uploaded.url; } finally { await rm(dir, { recursive: true, force: true }); } })(); pending.set(url, job); try { return await job; } finally { pending.delete(url); } }