| import { execFile } from "child_process"; |
| import { promisify } from "util"; |
| import ffmpegStatic from "ffmpeg-static"; |
| import ffprobeStatic from "ffprobe-static"; |
| import type { VideoMeta } from "@/types"; |
|
|
| const execFileAsync = promisify(execFile); |
|
|
| export function getFfmpegPath(): string { |
| if (!ffmpegStatic) throw new Error("ffmpeg-static not available"); |
| return ffmpegStatic; |
| } |
|
|
| export function getFfprobePath(): string { |
| return ffprobeStatic.path; |
| } |
|
|
| function parseFps(rate: string): number { |
| if (!rate || rate === "0/0") return 30; |
| const [num, den] = rate.split("/").map(Number); |
| if (!den) return 30; |
| return Math.round((num / den) * 100) / 100; |
| } |
|
|
| export async function extractVideoMeta(videoFile: string): Promise<VideoMeta> { |
| const ffprobe = getFfprobePath(); |
| const { stdout } = await execFileAsync(ffprobe, [ |
| "-v", "error", |
| "-select_streams", "v:0", |
| "-show_entries", "stream=width,height,r_frame_rate,duration", |
| "-show_entries", "format=duration", |
| "-of", "json", |
| videoFile, |
| ]); |
|
|
| const data = JSON.parse(stdout); |
| const stream = data.streams?.[0] ?? {}; |
| const formatDuration = Number(data.format?.duration ?? 0); |
| const streamDuration = Number(stream.duration ?? 0); |
| const duration = streamDuration || formatDuration || 0; |
|
|
| return { |
| width: Number(stream.width ?? 1920), |
| height: Number(stream.height ?? 1080), |
| duration, |
| fps: parseFps(stream.r_frame_rate ?? "30/1"), |
| }; |
| } |
|
|
| export async function runFfmpeg(args: string[]): Promise<void> { |
| await execFileAsync(getFfmpegPath(), args); |
| } |
|
|
| export async function extractFrames( |
| inputVideo: string, |
| outputPattern: string, |
| fps: number, |
| width: number, |
| ): Promise<void> { |
| await runFfmpeg([ |
| "-i", inputVideo, |
| "-vf", `fps=${fps},scale=${width}:-1:flags=lanczos`, |
| "-q:v", "2", |
| "-y", |
| outputPattern, |
| ]); |
| } |
|
|
| export async function encodeVideo( |
| inputVideo: string, |
| framePattern: string, |
| outputVideo: string, |
| fps: number, |
| audioFile?: string, |
| ): Promise<void> { |
| const args = [ |
| "-framerate", String(fps), |
| "-i", framePattern, |
| ]; |
|
|
| if (audioFile) { |
| args.push("-i", audioFile, "-map", "0:v", "-map", "1:a"); |
| } else { |
| args.push("-i", inputVideo, "-map", "0:v", "-map", "1:a?"); |
| } |
|
|
| args.push( |
| "-c:v", "libx264", |
| "-preset", "fast", |
| "-crf", "23", |
| "-pix_fmt", "yuv420p", |
| "-c:a", "aac", |
| "-b:a", "128k", |
| "-movflags", "+faststart", |
| "-shortest", |
| "-y", |
| outputVideo, |
| ); |
|
|
| await runFfmpeg(args); |
| } |
|
|