UNPKG

ffmpeg-forge

Version:

A modern, type-safe FFmpeg wrapper for Node.js with zero dependencies

1,591 lines 144 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const events = require("events"); const child_process = require("child_process"); const fs = require("fs"); const os = require("os"); const path = require("path"); class CommandBuilder { constructor() { __publicField(this, "args", []); } /** * Add global options (before input) */ addGlobalOptions(options) { if (options.overwrite) { this.args.push("-y"); } if (options.hideOutput) { this.args.push("-hide_banner", "-loglevel", "error"); } return this; } /** * Add input file */ addInput(inputPath) { this.args.push("-i", inputPath); return this; } /** * Add video codec */ addVideoCodec(codec) { this.args.push("-c:v", codec); return this; } /** * Add audio codec */ addAudioCodec(codec) { this.args.push("-c:a", codec); return this; } /** * Add video bitrate */ addVideoBitrate(bitrate) { this.args.push("-b:v", bitrate); return this; } /** * Add audio bitrate */ addAudioBitrate(bitrate) { this.args.push("-b:a", bitrate); return this; } /** * Add output format */ addFormat(format) { this.args.push("-f", format); return this; } /** * Add FPS */ addFps(fps) { this.args.push("-r", fps.toString()); return this; } /** * Add video size/resolution */ addSize(size) { this.args.push("-s", size); return this; } /** * Add aspect ratio */ addAspectRatio(aspectRatio) { this.args.push("-aspect", aspectRatio); return this; } /** * Add duration (limit output duration) */ addDuration(duration) { this.args.push("-t", duration.toString()); return this; } /** * Add seek (start position) */ addSeek(seek) { this.args.push("-ss", seek.toString()); return this; } /** * Disable audio */ disableAudio() { this.args.push("-an"); return this; } /** * Disable video */ disableVideo() { this.args.push("-vn"); return this; } /** * Add custom arguments */ addCustomArgs(customArgs) { this.args.push(...customArgs); return this; } /** * Add output file */ addOutput(outputPath) { this.args.push(outputPath); return this; } /** * Build FFmpeg conversion command with options */ buildConversionCommand(inputPath, outputPath, options = {}) { this.reset(); this.addGlobalOptions({ overwrite: true }); this.addInput(inputPath); if (options.videoCodec) { this.addVideoCodec(options.videoCodec); } if (options.audioCodec) { this.addAudioCodec(options.audioCodec); } if (options.videoBitrate) { this.addVideoBitrate(options.videoBitrate); } if (options.audioBitrate) { this.addAudioBitrate(options.audioBitrate); } if (options.format) { this.addFormat(options.format); } if (options.fps) { this.addFps(options.fps); } if (options.size) { this.addSize(options.size); } if (options.aspectRatio) { this.addAspectRatio(options.aspectRatio); } if (options.seek) { this.addSeek(options.seek); } if (options.duration) { this.addDuration(options.duration); } if (options.noAudio) { this.disableAudio(); } if (options.noVideo) { this.disableVideo(); } this.addOutput(outputPath); return this.build(); } /** * Build FFprobe command for metadata extraction */ buildProbeCommand(inputPath) { this.reset(); return ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", inputPath]; } /** * Build the command arguments array */ build() { return [...this.args]; } /** * Reset the builder */ reset() { this.args = []; return this; } /** * Get the command as a string (for display purposes) */ toString(executable = "ffmpeg") { return `${executable} ${this.args.join(" ")}`; } } function parseVersion(output) { const lines = output.split("\n"); const versionLine = lines[0]; const versionMatch = versionLine.match(/ffmpeg version ([^\s]+)/); const version = versionMatch ? versionMatch[1] : "unknown"; const copyrightMatch = versionLine.match(/Copyright \(c\) (.+)/); const copyright = copyrightMatch ? copyrightMatch[1] : ""; const configLines = lines.filter((line) => line.trim().startsWith("--")); const configuration = configLines.map((line) => line.trim()); const libVersions = {}; const libLines = lines.filter((line) => line.trim().match(/^lib\w+\s+\d+\./)); libLines.forEach((line) => { const match = line.trim().match(/^(lib\w+)\s+([\d.]+)/); if (match) { libVersions[match[1]] = match[2]; } }); return { version, copyright, configuration, libVersions }; } function parseFormats(output) { const lines = output.split("\n"); const demuxing = []; const muxing = []; let startParsing = false; for (const line of lines) { if (line.includes("--")) { startParsing = true; continue; } if (!startParsing || !line.trim()) continue; const match = line.match(/^\s*([DE\s]{2})\s+(\S+)/); if (match) { const flags = match[1]; const format = match[2]; if (flags.includes("D")) demuxing.push(format); if (flags.includes("E")) muxing.push(format); } } return { demuxing, muxing }; } function parseEncoders(output) { const lines = output.split("\n"); const video = []; const audio = []; const subtitle = []; let startParsing = false; for (const line of lines) { if (line.includes("------")) { startParsing = true; continue; } if (!startParsing || !line.trim()) continue; const match = line.match(/^\s*([VAS][.FXBD]{5})\s+(\S+)/); if (match) { const flags = match[1]; const codec = match[2]; const codecType = flags.charAt(0); if (codecType === "V") { video.push(codec); } else if (codecType === "A") { audio.push(codec); } else if (codecType === "S") { subtitle.push(codec); } } } return { video, audio, subtitle }; } function parseDecoders(output) { const lines = output.split("\n"); const video = []; const audio = []; const subtitle = []; let startParsing = false; for (const line of lines) { if (line.includes("------")) { startParsing = true; continue; } if (!startParsing || !line.trim()) continue; const match = line.match(/^\s*([VAS][.FXBD]{5})\s+(\S+)/); if (match) { const flags = match[1]; const codec = match[2]; const codecType = flags.charAt(0); if (codecType === "V") { video.push(codec); } else if (codecType === "A") { audio.push(codec); } else if (codecType === "S") { subtitle.push(codec); } } } return { video, audio, subtitle }; } function parseMediaMetadata(jsonOutput) { const data = JSON.parse(jsonOutput); const format = { filename: data.format.filename, formatName: data.format.format_name, formatLongName: data.format.format_long_name, startTime: data.format.start_time, duration: data.format.duration, size: data.format.size, bitRate: data.format.bit_rate, probeScore: data.format.probe_score, tags: data.format.tags || {} }; const streams = (data.streams || []).map((stream) => ({ index: stream.index, codecName: stream.codec_name, codecLongName: stream.codec_long_name, codecType: stream.codec_type, codecTag: stream.codec_tag_string, // Video specific width: stream.width, height: stream.height, codedWidth: stream.coded_width, codedHeight: stream.coded_height, displayAspectRatio: stream.display_aspect_ratio, pixelFormat: stream.pix_fmt, frameRate: stream.r_frame_rate, avgFrameRate: stream.avg_frame_rate, // Audio specific sampleRate: stream.sample_rate, channels: stream.channels, channelLayout: stream.channel_layout, bitsPerSample: stream.bits_per_sample, // Common duration: stream.duration, durationTs: stream.duration_ts, startTime: stream.start_time, startPts: stream.start_pts, bitrate: stream.bit_rate, tags: stream.tags || {} })); return { format, streams }; } function parseVideoMetadata(metadata) { const videoStreams = metadata.streams.filter((s) => s.codecType === "video"); const audioStreams = metadata.streams.filter((s) => s.codecType === "audio"); const subtitleStreams = metadata.streams.filter((s) => s.codecType === "subtitle"); if (videoStreams.length === 0) { throw new Error("No video stream found in media file"); } const primaryVideo = videoStreams[0]; const primaryAudio = audioStreams[0]; let frameRate = 0; if (primaryVideo.avgFrameRate) { const [num, den] = primaryVideo.avgFrameRate.split("/").map(Number); frameRate = den > 0 ? num / den : 0; } const duration = parseFloat(metadata.format.duration || "0"); const bitrate = parseInt(metadata.format.bitRate || "0", 10) / 1e3; const size = parseInt(metadata.format.size || "0", 10); return { format: metadata.format, videoStreams, audioStreams, subtitleStreams, duration, width: primaryVideo.width || 0, height: primaryVideo.height || 0, frameRate, videoCodec: primaryVideo.codecName, audioCodec: primaryAudio == null ? void 0 : primaryAudio.codecName, bitrate, size }; } function parseImageMetadata(metadata) { const videoStreams = metadata.streams.filter((s) => s.codecType === "video"); if (videoStreams.length === 0) { throw new Error("No image stream found in file"); } const primaryStream = videoStreams[0]; const size = parseInt(metadata.format.size || "0", 10); return { format: metadata.format, width: primaryStream.width || 0, height: primaryStream.height || 0, pixelFormat: primaryStream.pixelFormat || "unknown", codec: primaryStream.codecName, size }; } var HardwareAcceleration = /* @__PURE__ */ ((HardwareAcceleration2) => { HardwareAcceleration2["CPU"] = "cpu"; HardwareAcceleration2["NVIDIA"] = "nvidia"; HardwareAcceleration2["INTEL"] = "intel"; HardwareAcceleration2["AMD"] = "amd"; HardwareAcceleration2["VAAPI"] = "vaapi"; HardwareAcceleration2["VIDEOTOOLBOX"] = "videotoolbox"; HardwareAcceleration2["V4L2"] = "v4l2"; HardwareAcceleration2["ANY"] = "any"; return HardwareAcceleration2; })(HardwareAcceleration || {}); const GPU_CODEC_PATTERNS = { nvidia: ["_nvenc", "_cuvid"], intel: ["_qsv"], amd: ["_amf"], vaapi: ["_vaapi"], videotoolbox: ["_videotoolbox"], v4l2: ["_v4l2m2m"] }; var StreamType = /* @__PURE__ */ ((StreamType2) => { StreamType2["VIDEO"] = "video"; StreamType2["AUDIO"] = "audio"; StreamType2["SUBTITLE"] = "subtitle"; return StreamType2; })(StreamType || {}); function isGPUCodec(codec) { return Object.values(GPU_CODEC_PATTERNS).flat().some((pattern) => codec.includes(pattern)); } function isCPUCodec(codec) { return !isGPUCodec(codec); } function detectHardwareType(codec) { for (const [type, patterns] of Object.entries(GPU_CODEC_PATTERNS)) { if (patterns.some((pattern) => codec.includes(pattern))) { return type; } } return HardwareAcceleration.CPU; } function filterCodecsByAcceleration(codecs, acceleration) { const accelValue = typeof acceleration === "string" ? acceleration : acceleration; if (accelValue === HardwareAcceleration.ANY || accelValue === "any") { return codecs; } if (accelValue === HardwareAcceleration.CPU || accelValue === "cpu") { return codecs.filter(isCPUCodec); } const patterns = GPU_CODEC_PATTERNS[accelValue]; if (!patterns) { return []; } return codecs.filter((codec) => patterns.some((pattern) => codec.includes(pattern))); } function getInputType(input) { if (typeof input === "string") { return "path"; } else if (Buffer.isBuffer(input)) { return "buffer"; } else { return "stream"; } } async function prepareInput(input) { const type = getInputType(input); if (type === "path") { return { type: "path", source: input }; } const tempPath = path.join( os.tmpdir(), `ffmpeg-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp` ); if (type === "buffer") { fs.writeFileSync(tempPath, input); return { type: "buffer", source: input, tempPath }; } if (type === "stream") { const stream = input; return new Promise((resolve, reject) => { const chunks = []; stream.on("data", (chunk) => { chunks.push(Buffer.from(chunk)); }); stream.on("end", () => { const buffer = Buffer.concat(chunks); fs.writeFileSync(tempPath, buffer); resolve({ type: "stream", source: input, tempPath }); }); stream.on("error", (error) => { reject(error); }); }); } throw new Error(`Unsupported input type: ${typeof input}`); } function cleanupInput(inputInfo) { if (inputInfo.tempPath) { try { fs.unlinkSync(inputInfo.tempPath); } catch { } } } function getInputPath(inputInfo) { return inputInfo.tempPath || inputInfo.source; } var VideoCodec = /* @__PURE__ */ ((VideoCodec2) => { VideoCodec2["H264"] = "libx264"; VideoCodec2["H264_RGB"] = "libx264rgb"; VideoCodec2["H265"] = "libx265"; VideoCodec2["VP8"] = "libvpx"; VideoCodec2["VP9"] = "libvpx-vp9"; VideoCodec2["AV1_LIBAOM"] = "libaom-av1"; VideoCodec2["AV1_SVT"] = "libsvtav1"; VideoCodec2["MPEG4"] = "mpeg4"; VideoCodec2["MPEG2"] = "mpeg2video"; VideoCodec2["H264_NVENC"] = "h264_nvenc"; VideoCodec2["HEVC_NVENC"] = "hevc_nvenc"; VideoCodec2["AV1_NVENC"] = "av1_nvenc"; VideoCodec2["H264_QSV"] = "h264_qsv"; VideoCodec2["HEVC_QSV"] = "hevc_qsv"; VideoCodec2["AV1_QSV"] = "av1_qsv"; VideoCodec2["H264_AMF"] = "h264_amf"; VideoCodec2["HEVC_AMF"] = "hevc_amf"; VideoCodec2["H264_VAAPI"] = "h264_vaapi"; VideoCodec2["HEVC_VAAPI"] = "hevc_vaapi"; VideoCodec2["VP8_VAAPI"] = "vp8_vaapi"; VideoCodec2["VP9_VAAPI"] = "vp9_vaapi"; VideoCodec2["AV1_VAAPI"] = "av1_vaapi"; VideoCodec2["H264_VIDEOTOOLBOX"] = "h264_videotoolbox"; VideoCodec2["HEVC_VIDEOTOOLBOX"] = "hevc_videotoolbox"; VideoCodec2["COPY"] = "copy"; return VideoCodec2; })(VideoCodec || {}); var AudioCodec = /* @__PURE__ */ ((AudioCodec2) => { AudioCodec2["AAC"] = "aac"; AudioCodec2["MP3"] = "libmp3lame"; AudioCodec2["OPUS"] = "libopus"; AudioCodec2["VORBIS"] = "libvorbis"; AudioCodec2["FLAC"] = "flac"; AudioCodec2["AC3"] = "ac3"; AudioCodec2["EAC3"] = "eac3"; AudioCodec2["DTS"] = "dca"; AudioCodec2["COPY"] = "copy"; return AudioCodec2; })(AudioCodec || {}); var OutputFormat = /* @__PURE__ */ ((OutputFormat2) => { OutputFormat2["MP4"] = "mp4"; OutputFormat2["WEBM"] = "webm"; OutputFormat2["MKV"] = "matroska"; OutputFormat2["AVI"] = "avi"; OutputFormat2["MOV"] = "mov"; OutputFormat2["FLV"] = "flv"; OutputFormat2["MPEG"] = "mpeg"; OutputFormat2["MPEGTS"] = "mpegts"; OutputFormat2["OGV"] = "ogg"; OutputFormat2["MP3"] = "mp3"; OutputFormat2["AAC"] = "aac"; OutputFormat2["OGG"] = "ogg"; OutputFormat2["OPUS"] = "opus"; OutputFormat2["FLAC"] = "flac"; OutputFormat2["WAV"] = "wav"; OutputFormat2["M4A"] = "m4a"; OutputFormat2["GIF"] = "gif"; OutputFormat2["APNG"] = "apng"; OutputFormat2["PNG"] = "image2"; OutputFormat2["JPEG"] = "image2"; return OutputFormat2; })(OutputFormat || {}); const FORMAT_COMPATIBILITY = { mp4: ["mp4", "webm", "mkv", "avi", "mov", "flv", "ogv"], webm: ["webm", "mp4", "mkv", "ogv"], mkv: ["mkv", "mp4", "webm", "avi", "mov"], matroska: ["mkv", "mp4", "webm", "avi", "mov"], avi: ["avi", "mp4", "mkv", "mov"], mov: ["mov", "mp4", "mkv", "webm"], flv: ["flv", "mp4", "mkv"], ogv: ["ogv", "webm", "mkv"] }; const CODEC_CONTAINER_COMPATIBILITY = { h264: ["mp4", "mkv", "mov", "avi", "flv", "ts", "m4v"], hevc: ["mp4", "mkv", "mov", "ts"], vp8: ["webm", "mkv"], vp9: ["webm", "mkv"], av1: ["webm", "mkv", "mp4"], mpeg4: ["mp4", "avi", "mkv", "3gp"], aac: ["mp4", "mov", "mkv", "flv", "m4a"], opus: ["webm", "mkv", "ogg"], vorbis: ["webm", "mkv", "ogg"], mp3: ["mp3", "mp4", "mkv", "avi"] }; function generateConversionSuggestions(metadata, availableFormats, availableCodecs) { const currentFormat = metadata.format.formatName.split(",")[0]; const currentVideoCodec = metadata.videoCodec; const currentAudioCodec = metadata.audioCodec; const currentResolution = `${metadata.width}x${metadata.height}`; const baseCompatibleFormats = FORMAT_COMPATIBILITY[currentFormat] || Object.keys(FORMAT_COMPATIBILITY); const suggestedFormats = baseCompatibleFormats.filter( (fmt) => availableFormats.muxing.includes(fmt) ); const cpuCodecs = availableCodecs.video.encoders.filter( (codec) => !codec.includes("_nvenc") && !codec.includes("_qsv") && !codec.includes("_amf") && !codec.includes("_vaapi") && !codec.includes("_v4l2m2m") && !codec.includes("_videotoolbox") ); const gpuCodecs = availableCodecs.video.encoders.filter( (codec) => codec.includes("_nvenc") || codec.includes("_qsv") || codec.includes("_amf") || codec.includes("_vaapi") || codec.includes("_videotoolbox") ); const suggestedAudioCodecs = availableCodecs.audio.encoders.filter( (codec) => ["aac", "libmp3lame", "libopus", "libvorbis", "flac", "ac3"].some( (common) => codec.includes(common) ) ); const canTranscode = cpuCodecs.length > 0 || gpuCodecs.length > 0; const codecContainers = CODEC_CONTAINER_COMPATIBILITY[currentVideoCodec] || []; const canRemux = codecContainers.some((fmt) => availableFormats.muxing.includes(fmt)); return { currentFormat, currentVideoCodec, currentAudioCodec, currentResolution, suggestedFormats, suggestedVideoCodecs: { cpu: cpuCodecs, gpu: gpuCodecs }, suggestedAudioCodecs, canTranscode, canRemux }; } function checkConversionCompatibility(sourceVideoCodec, sourceAudioCodec, sourceFormat, targetVideoCodec, targetAudioCodec, targetFormat) { var _a; const warnings = []; let estimatedQuality = "high"; const videoCodecMatch = sourceVideoCodec === targetVideoCodec || targetVideoCodec === "copy"; const audioCodecMatch = sourceAudioCodec === targetAudioCodec || targetAudioCodec === "copy"; const canDirectCopy = videoCodecMatch && audioCodecMatch; const requiresTranscode = !canDirectCopy; const formatCompatible = ((_a = FORMAT_COMPATIBILITY[sourceFormat]) == null ? void 0 : _a.includes(targetFormat)) ?? true; if (canDirectCopy) { estimatedQuality = "lossless"; } else if (targetVideoCodec.includes("ffv1") || targetVideoCodec.includes("copy")) { estimatedQuality = "lossless"; } else if (targetVideoCodec.includes("264") || targetVideoCodec.includes("265") || targetVideoCodec.includes("vp9")) { estimatedQuality = "high"; } else if (targetVideoCodec.includes("vp8") || targetVideoCodec.includes("mpeg4")) { estimatedQuality = "medium"; } else { estimatedQuality = "medium"; } if (requiresTranscode) { warnings.push("Transcoding required - may lose quality"); } if (!formatCompatible) { warnings.push("Format combination may have compatibility issues"); } if (sourceVideoCodec.includes("hevc") && targetFormat === "avi") { warnings.push("HEVC to AVI conversion may have limited support"); } if (targetAudioCodec && sourceAudioCodec && !audioCodecMatch) { warnings.push("Audio will be re-encoded"); } return { sourceFormat, targetFormat, sourceVideoCodec, targetVideoCodec, sourceAudioCodec, targetAudioCodec, compatible: true, // Most conversions are technically possible requiresTranscode, canDirectCopy, estimatedQuality, warnings }; } function getConversionRecommendation(_metadata, useCase = "web") { switch (useCase) { case "web": return { recommended: true, format: OutputFormat.MP4, videoCodec: VideoCodec.H264, audioCodec: AudioCodec.AAC, reason: "Best browser compatibility and streaming support", alternatives: [ { format: OutputFormat.WEBM, videoCodec: VideoCodec.VP9, audioCodec: AudioCodec.OPUS, reason: "Modern browsers, better compression" }, { format: OutputFormat.MP4, videoCodec: VideoCodec.H265, audioCodec: AudioCodec.AAC, reason: "Better quality/size ratio, requires modern browsers" } ] }; case "mobile": return { recommended: true, format: OutputFormat.MP4, videoCodec: VideoCodec.H264, audioCodec: AudioCodec.AAC, reason: "Universal mobile device support", alternatives: [ { format: OutputFormat.MP4, videoCodec: VideoCodec.H264_VIDEOTOOLBOX, audioCodec: AudioCodec.AAC, reason: "Hardware acceleration on iOS devices" } ] }; case "quality": return { recommended: true, format: OutputFormat.MKV, videoCodec: VideoCodec.H265, audioCodec: AudioCodec.FLAC, reason: "Best quality with efficient compression", alternatives: [ { format: OutputFormat.MP4, videoCodec: VideoCodec.H265, audioCodec: AudioCodec.AAC, reason: "High quality with better compatibility" }, { format: OutputFormat.WEBM, videoCodec: VideoCodec.VP9, audioCodec: AudioCodec.OPUS, reason: "Excellent quality, open format" } ] }; case "size": return { recommended: true, format: OutputFormat.WEBM, videoCodec: VideoCodec.VP9, audioCodec: AudioCodec.OPUS, reason: "Best compression efficiency", alternatives: [ { format: OutputFormat.MP4, videoCodec: VideoCodec.H265, audioCodec: AudioCodec.AAC, reason: "Excellent compression, better compatibility" } ] }; case "compatibility": return { recommended: true, format: OutputFormat.MP4, videoCodec: VideoCodec.H264, audioCodec: AudioCodec.AAC, reason: "Works everywhere - maximum compatibility", alternatives: [ { format: OutputFormat.AVI, videoCodec: VideoCodec.MPEG4, audioCodec: AudioCodec.MP3, reason: "Legacy system support" } ] }; default: return { recommended: true, format: OutputFormat.MP4, videoCodec: VideoCodec.H264, audioCodec: AudioCodec.AAC, reason: "General purpose - good balance", alternatives: [] }; } } class FilterBuilder { /** * Build scale filter */ static buildScaleFilter(scale) { const parts = []; const width = scale.width ?? -1; const height = scale.height ?? -1; parts.push(`${width}:${height}`); if (scale.algorithm) { parts.push(`flags=${scale.algorithm}`); } if (scale.force_original_aspect_ratio) { parts.push(`force_original_aspect_ratio=${scale.force_original_aspect_ratio}`); } if (scale.force_divisible_by) { parts.push(`force_divisible_by=${scale.force_divisible_by}`); } return `scale=${parts.join(":")}`; } /** * Build crop filter */ static buildCropFilter(crop) { const w = crop.width; const h = crop.height; const x = crop.x ?? "(iw-w)/2"; const y = crop.y ?? "(ih-h)/2"; return `crop=${w}:${h}:${x}:${y}`; } /** * Build pad filter */ static buildPadFilter(pad) { const parts = [pad.width.toString(), pad.height.toString()]; if (pad.x !== void 0) parts.push(pad.x.toString()); if (pad.y !== void 0) parts.push(pad.y.toString()); if (pad.color) parts.push(pad.color); return `pad=${parts.join(":")}`; } /** * Build deinterlace filter */ static buildDeinterlaceFilter(deinterlace) { const mode = deinterlace.mode || "yadif"; const parts = []; if (deinterlace.parity) { const parityMap = { tff: "0", bff: "1", auto: "-1" }; parts.push(parityMap[deinterlace.parity] || "-1"); } if (deinterlace.deint) { const deintMap = { all: "0", interlaced: "1" }; parts.push(deintMap[deinterlace.deint] || "0"); } return parts.length > 0 ? `${mode}=${parts.join(":")}` : mode; } /** * Build denoise filter */ static buildDenoiseFilter(denoise) { const filter = denoise.filter || "hqdn3d"; const parts = []; if (denoise.luma_spatial !== void 0) parts.push(denoise.luma_spatial.toString()); if (denoise.chroma_spatial !== void 0) parts.push(denoise.chroma_spatial.toString()); if (denoise.luma_tmp !== void 0) parts.push(denoise.luma_tmp.toString()); if (denoise.chroma_tmp !== void 0) parts.push(denoise.chroma_tmp.toString()); return parts.length > 0 ? `${filter}=${parts.join(":")}` : filter; } /** * Build sharpen (unsharp) filter */ static buildSharpenFilter(sharpen) { const parts = []; if (sharpen.luma_msize_x !== void 0) parts.push(`luma_msize_x=${sharpen.luma_msize_x}`); if (sharpen.luma_msize_y !== void 0) parts.push(`luma_msize_y=${sharpen.luma_msize_y}`); if (sharpen.luma_amount !== void 0) parts.push(`luma_amount=${sharpen.luma_amount}`); if (sharpen.chroma_msize_x !== void 0) parts.push(`chroma_msize_x=${sharpen.chroma_msize_x}`); if (sharpen.chroma_msize_y !== void 0) parts.push(`chroma_msize_y=${sharpen.chroma_msize_y}`); if (sharpen.chroma_amount !== void 0) parts.push(`chroma_amount=${sharpen.chroma_amount}`); return parts.length > 0 ? `unsharp=${parts.join(":")}` : "unsharp"; } /** * Build color (eq) filter */ static buildColorFilter(color) { const parts = []; if (color.brightness !== void 0) parts.push(`brightness=${color.brightness}`); if (color.contrast !== void 0) parts.push(`contrast=${color.contrast}`); if (color.saturation !== void 0) parts.push(`saturation=${color.saturation}`); if (color.gamma !== void 0) parts.push(`gamma=${color.gamma}`); if (color.gamma_r !== void 0) parts.push(`gamma_r=${color.gamma_r}`); if (color.gamma_g !== void 0) parts.push(`gamma_g=${color.gamma_g}`); if (color.gamma_b !== void 0) parts.push(`gamma_b=${color.gamma_b}`); return `eq=${parts.join(":")}`; } /** * Build rotate filter */ static buildRotateFilter(rotate) { const parts = [`a=${rotate.angle}`]; if (rotate.fillcolor) parts.push(`fillcolor=${rotate.fillcolor}`); if (rotate.bilinear !== void 0) parts.push(`bilinear=${rotate.bilinear ? "1" : "0"}`); return `rotate=${parts.join(":")}`; } /** * Build flip filter */ static buildFlipFilter(flip) { const filters = []; if (flip.horizontal) filters.push("hflip"); if (flip.vertical) filters.push("vflip"); return filters.join(","); } /** * Build watermark (overlay) filter */ static buildWatermarkFilter(watermark) { const parts = []; if (watermark.x !== void 0) parts.push(`x=${watermark.x}`); if (watermark.y !== void 0) parts.push(`y=${watermark.y}`); if (watermark.opacity !== void 0) { return `overlay=${parts.join(":")}:format=auto:alpha=${watermark.opacity}`; } if (watermark.enable) parts.push(`enable='${watermark.enable}'`); return `overlay=${parts.join(":")}`; } /** * Build text (drawtext) filter */ static buildTextFilter(text) { const parts = [`text='${text.text.replace(/'/g, "\\'")}'`]; if (text.fontfile) parts.push(`fontfile=${text.fontfile}`); if (text.fontsize) parts.push(`fontsize=${text.fontsize}`); if (text.fontcolor) parts.push(`fontcolor=${text.fontcolor}`); if (text.x !== void 0) parts.push(`x=${text.x}`); if (text.y !== void 0) parts.push(`y=${text.y}`); if (text.shadowcolor) parts.push(`shadowcolor=${text.shadowcolor}`); if (text.shadowx !== void 0) parts.push(`shadowx=${text.shadowx}`); if (text.shadowy !== void 0) parts.push(`shadowy=${text.shadowy}`); if (text.borderw !== void 0) parts.push(`borderw=${text.borderw}`); if (text.bordercolor) parts.push(`bordercolor=${text.bordercolor}`); return `drawtext=${parts.join(":")}`; } /** * Build fade filter */ static buildFadeFilter(fade) { const parts = [`type=${fade.type}`]; if (fade.start_frame !== void 0) parts.push(`start_frame=${fade.start_frame}`); if (fade.nb_frames !== void 0) parts.push(`nb_frames=${fade.nb_frames}`); if (fade.start_time !== void 0) parts.push(`start_time=${fade.start_time}`); if (fade.duration !== void 0) parts.push(`duration=${fade.duration}`); if (fade.color) parts.push(`color=${fade.color}`); return `fade=${parts.join(":")}`; } /** * Build volume filter */ static buildVolumeFilter(volume) { const parts = [`volume=${volume.volume}`]; if (volume.precision) parts.push(`precision=${volume.precision}`); return parts.join(":"); } /** * Build audio denoise filter */ static buildAudioDenoiseFilter(denoise) { const parts = []; if (denoise.noise_reduction !== void 0) { parts.push(`nr=${denoise.noise_reduction}`); } if (denoise.noise_type) { parts.push(`nf=${denoise.noise_type}`); } return parts.length > 0 ? `afftdn=${parts.join(":")}` : "afftdn"; } /** * Build equalizer filter */ static buildEqualizerFilter(eq) { const parts = [`f=${eq.frequency}`]; if (eq.width_type) parts.push(`t=${eq.width_type}`); if (eq.width !== void 0) parts.push(`w=${eq.width}`); if (eq.gain !== void 0) parts.push(`g=${eq.gain}`); return `equalizer=${parts.join(":")}`; } /** * Build tempo filter */ static buildTempoFilter(tempo) { return `atempo=${tempo.tempo}`; } /** * Build pitch filter */ static buildPitchFilter(pitch) { return `asetrate=44100*2^(${pitch.pitch}/12),aresample=44100`; } /** * Build upscaling filter chain */ static buildUpscaleFilter(upscale) { const filters = []; if (upscale.denoiseBeforeScale) { filters.push("hqdn3d=4:3:6:4.5"); } const scaleFilter = `scale=${upscale.targetWidth}:${upscale.targetHeight}:flags=${upscale.algorithm}`; filters.push(scaleFilter); if (upscale.enhanceSharpness) { const amount = upscale.sharpnessAmount || 1; filters.push(`unsharp=5:5:${amount}:5:5:0.0`); } return filters; } /** * Build downscaling filter chain */ static buildDownscaleFilter(downscale) { const filters = []; if (downscale.deinterlace) { filters.push("yadif=0:-1:0"); } const algorithm = downscale.preserveDetails ? "lanczos" : downscale.algorithm; const scaleFilter = `scale=${downscale.targetWidth}:${downscale.targetHeight}:flags=${algorithm}`; filters.push(scaleFilter); return filters; } /** * Build complete video filter chain */ static buildVideoFilters(filters) { const filterArray = []; if (filters.deinterlace) { filterArray.push(this.buildDeinterlaceFilter(filters.deinterlace)); } if (filters.crop) { filterArray.push(this.buildCropFilter(filters.crop)); } if (filters.denoise) { filterArray.push(this.buildDenoiseFilter(filters.denoise)); } if (filters.scale) { filterArray.push(this.buildScaleFilter(filters.scale)); } if (filters.pad) { filterArray.push(this.buildPadFilter(filters.pad)); } if (filters.color) { filterArray.push(this.buildColorFilter(filters.color)); } if (filters.sharpen) { filterArray.push(this.buildSharpenFilter(filters.sharpen)); } if (filters.rotate) { filterArray.push(this.buildRotateFilter(filters.rotate)); } if (filters.flip) { const flipFilters = this.buildFlipFilter(filters.flip); if (flipFilters) filterArray.push(flipFilters); } if (filters.text) { filterArray.push(this.buildTextFilter(filters.text)); } if (filters.fade) { filterArray.push(this.buildFadeFilter(filters.fade)); } if (filters.custom) { filterArray.push(...filters.custom); } return filterArray.join(","); } /** * Build complete audio filter chain */ static buildAudioFilters(filters) { const filterArray = []; if (filters.denoise) { filterArray.push(this.buildAudioDenoiseFilter(filters.denoise)); } if (filters.equalizer) { filters.equalizer.forEach((eq) => { filterArray.push(this.buildEqualizerFilter(eq)); }); } if (filters.tempo) { filterArray.push(this.buildTempoFilter(filters.tempo)); } if (filters.pitch) { filterArray.push(this.buildPitchFilter(filters.pitch)); } if (filters.volume) { filterArray.push(this.buildVolumeFilter(filters.volume)); } if (filters.custom) { filterArray.push(...filters.custom); } return filterArray.join(","); } /** * Build complex filter graph */ static buildComplexFilter(specs) { return specs.map((spec) => { var _a, _b; const inputs = ((_a = spec.inputs) == null ? void 0 : _a.map((i) => `[${i}]`).join("")) || ""; const outputs = ((_b = spec.outputs) == null ? void 0 : _b.map((o) => `[${o}]`).join("")) || ""; let filter = spec.filter; if (spec.options && Object.keys(spec.options).length > 0) { const opts = Object.entries(spec.options).map(([k, v]) => `${k}=${v}`).join(":"); filter = `${filter}=${opts}`; } return `${inputs}${filter}${outputs}`; }).join(";"); } } const HARDWARE_CODEC_MAP = { // NVIDIA NVENC [HardwareAcceleration.NVIDIA]: { h264: VideoCodec.H264_NVENC, libx264: VideoCodec.H264_NVENC, h265: VideoCodec.HEVC_NVENC, libx265: VideoCodec.HEVC_NVENC, hevc: VideoCodec.HEVC_NVENC, av1: VideoCodec.AV1_NVENC }, nvenc: { h264: VideoCodec.H264_NVENC, libx264: VideoCodec.H264_NVENC, h265: VideoCodec.HEVC_NVENC, libx265: VideoCodec.HEVC_NVENC, hevc: VideoCodec.HEVC_NVENC, av1: VideoCodec.AV1_NVENC }, cuda: { h264: VideoCodec.H264_NVENC, libx264: VideoCodec.H264_NVENC, h265: VideoCodec.HEVC_NVENC, libx265: VideoCodec.HEVC_NVENC, hevc: VideoCodec.HEVC_NVENC, av1: VideoCodec.AV1_NVENC }, // Intel Quick Sync [HardwareAcceleration.INTEL]: { h264: VideoCodec.H264_QSV, libx264: VideoCodec.H264_QSV, h265: VideoCodec.HEVC_QSV, libx265: VideoCodec.HEVC_QSV, hevc: VideoCodec.HEVC_QSV, av1: VideoCodec.AV1_QSV, vp9: "vp9_qsv" }, qsv: { h264: VideoCodec.H264_QSV, libx264: VideoCodec.H264_QSV, h265: VideoCodec.HEVC_QSV, libx265: VideoCodec.HEVC_QSV, hevc: VideoCodec.HEVC_QSV, av1: VideoCodec.AV1_QSV, vp9: "vp9_qsv" }, // AMD AMF [HardwareAcceleration.AMD]: { h264: VideoCodec.H264_AMF, libx264: VideoCodec.H264_AMF, h265: VideoCodec.HEVC_AMF, libx265: VideoCodec.HEVC_AMF, hevc: VideoCodec.HEVC_AMF }, amf: { h264: VideoCodec.H264_AMF, libx264: VideoCodec.H264_AMF, h265: VideoCodec.HEVC_AMF, libx265: VideoCodec.HEVC_AMF, hevc: VideoCodec.HEVC_AMF }, // VAAPI (Linux) [HardwareAcceleration.VAAPI]: { h264: VideoCodec.H264_VAAPI, libx264: VideoCodec.H264_VAAPI, h265: VideoCodec.HEVC_VAAPI, libx265: VideoCodec.HEVC_VAAPI, hevc: VideoCodec.HEVC_VAAPI, vp8: VideoCodec.VP8_VAAPI, vp9: VideoCodec.VP9_VAAPI, av1: VideoCodec.AV1_VAAPI }, // VideoToolbox (macOS) [HardwareAcceleration.VIDEOTOOLBOX]: { h264: VideoCodec.H264_VIDEOTOOLBOX, libx264: VideoCodec.H264_VIDEOTOOLBOX, h265: VideoCodec.HEVC_VIDEOTOOLBOX, libx265: VideoCodec.HEVC_VIDEOTOOLBOX, hevc: VideoCodec.HEVC_VIDEOTOOLBOX } }; function detectHardwareAcceleration(ffmpegPath = "ffmpeg") { try { const output = child_process.execSync(`${ffmpegPath} -hide_banner -hwaccels`, { encoding: "utf-8" }); const lines = output.split("\n"); const available = []; for (const line of lines) { const trimmed = line.trim().toLowerCase(); if (trimmed === "cuda" || trimmed.includes("nvenc")) { if (!available.includes(HardwareAcceleration.NVIDIA)) { available.push(HardwareAcceleration.NVIDIA); } } else if (trimmed === "qsv") { available.push(HardwareAcceleration.INTEL); } else if (trimmed === "amf" || trimmed === "d3d11va") { if (!available.includes(HardwareAcceleration.AMD)) { available.push(HardwareAcceleration.AMD); } } else if (trimmed === "vaapi") { available.push(HardwareAcceleration.VAAPI); } else if (trimmed === "videotoolbox") { available.push(HardwareAcceleration.VIDEOTOOLBOX); } else if (trimmed === "dxva2") { available.push("dxva2"); } } return available; } catch { return []; } } function getHardwareCodec(cpuCodec, acceleration) { const codecMap = HARDWARE_CODEC_MAP[acceleration]; if (!codecMap) return null; const normalizedCodec = cpuCodec.toLowerCase().replace("lib", ""); return codecMap[normalizedCodec] || codecMap[cpuCodec] || null; } function isHardwareAccelerationAvailable(acceleration, ffmpegPath = "ffmpeg") { const available = detectHardwareAcceleration(ffmpegPath); return available.includes(acceleration); } function getBestHardwareAcceleration(ffmpegPath = "ffmpeg") { const available = detectHardwareAcceleration(ffmpegPath); if (available.length === 0) { return null; } const priority = [ HardwareAcceleration.NVIDIA, HardwareAcceleration.INTEL, HardwareAcceleration.AMD, HardwareAcceleration.VAAPI, HardwareAcceleration.VIDEOTOOLBOX ]; for (const hwAccel of priority) { if (available.includes(hwAccel)) { return hwAccel; } } return available[0]; } function autoSelectHardwareEncoding(desiredCodec, ffmpegPath = "ffmpeg") { const hwaccelMap = { [HardwareAcceleration.NVIDIA]: "cuda", [HardwareAcceleration.INTEL]: "qsv", [HardwareAcceleration.AMD]: "amf", [HardwareAcceleration.VAAPI]: "vaapi", [HardwareAcceleration.VIDEOTOOLBOX]: "videotoolbox", [HardwareAcceleration.V4L2]: "v4l2m2m" }; const hwAccel = getBestHardwareAcceleration(ffmpegPath); if (!hwAccel) { return { codec: desiredCodec, isHardware: false }; } const hwCodec = getHardwareCodec(desiredCodec, hwAccel); if (hwCodec) { return { codec: hwCodec, acceleration: hwAccel, ffmpegHwaccel: hwaccelMap[hwAccel] || hwAccel, isHardware: true }; } return { codec: desiredCodec, isHardware: false }; } function getHardwareAccelerationInfo(ffmpegPath = "ffmpeg") { const available = detectHardwareAcceleration(ffmpegPath); const best = getBestHardwareAcceleration(ffmpegPath); const capabilities = {}; available.forEach((hwAccel) => { capabilities[hwAccel] = Object.values(HARDWARE_CODEC_MAP[hwAccel] || {}); }); return { available, best, capabilities }; } class CommandGenerator { /** * Generate complete FFmpeg command arguments */ static generate(config, ffmpegPath = "ffmpeg") { var _a, _b, _c, _d, _e, _f; const args = []; args.push("-hide_banner"); const hwAccelMap = { nvidia: "cuda", intel: "qsv", amd: "amf", vaapi: "vaapi", videotoolbox: "videotoolbox", v4l2: "v4l2m2m" }; let hwAccelType; let useHardwareCodec = false; if (config.hardwareAcceleration) { if (typeof config.hardwareAcceleration === "string") { const mapped = hwAccelMap[config.hardwareAcceleration] || config.hardwareAcceleration; hwAccelType = mapped; useHardwareCodec = true; } else if (config.hardwareAcceleration.enabled) { const accelType = config.hardwareAcceleration.type; if (accelType) { hwAccelType = hwAccelMap[accelType] || accelType; } useHardwareCodec = config.hardwareAcceleration.preferHardware !== false; } } if (hwAccelType) { args.push("-hwaccel", hwAccelType); } let finalVideoConfig = config.video; if (useHardwareCodec && ((_a = config.video) == null ? void 0 : _a.codec) && !config.video.disabled) { const hwSelection = autoSelectHardwareEncoding(config.video.codec, ffmpegPath); if (hwSelection.isHardware) { finalVideoConfig = { ...config.video, codec: hwSelection.codec }; if (!hwAccelType && hwSelection.ffmpegHwaccel) { args.splice(2, 0, "-hwaccel", hwSelection.ffmpegHwaccel); } } else if (typeof config.hardwareAcceleration === "object" && config.hardwareAcceleration.fallbackToCPU === false) { throw new Error( `Hardware acceleration requested but codec ${config.video.codec} is not available with hardware acceleration` ); } } if (((_b = config.options) == null ? void 0 : _b.threads) !== void 0) { args.push("-threads", config.options.threads.toString()); } if ((_c = config.options) == null ? void 0 : _c.inputOptions) { args.push(...config.options.inputOptions); } if (((_d = config.timing) == null ? void 0 : _d.fastSeek) && ((_e = config.timing) == null ? void 0 : _e.seek) !== void 0) { args.push("-ss", this.formatTime(config.timing.seek)); } const inputPath = typeof config.input === "string" ? config.input : "pipe:0"; args.push("-i", inputPath); if (config.timing && !config.timing.fastSeek) { if (config.timing.seek !== void 0) { args.push("-ss", this.formatTime(config.timing.seek)); } if (config.timing.duration !== void 0) { args.push("-t", this.formatTime(config.timing.duration)); } else if (config.timing.to !== void 0) { args.push("-to", this.formatTime(config.timing.to)); } } if (finalVideoConfig) { this.addVideoArgs(args, finalVideoConfig); } if (config.audio) { this.addAudioArgs(args, config.audio); } if (config.format) { args.push("-f", config.format); } if (config.complexFilters && config.complexFilters.length > 0) { const complexFilter = FilterBuilder.buildComplexFilter(config.complexFilters); args.push("-filter_complex", complexFilter); } if (config.options) { this.addAdvancedOptions(args, config.options); } if ((_f = config.options) == null ? void 0 : _f.outputOptions) { args.push(...config.options.outputOptions); } const output = typeof config.output === "string" ? config.output : "pipe:1"; args.push("-y", output); return args; } /** * Add video-related arguments */ static addVideoArgs(args, video) { var _a, _b; if (video.disabled) { args.push("-vn"); return; } if (video.codec) { args.push("-c:v", video.codec); } if (video.bitrate) { const bitrate = typeof video.bitrate === "number" ? `${video.bitrate}` : video.bitrate; args.push("-b:v", bitrate); } if (video.quality !== void 0) { if (((_a = video.codec) == null ? void 0 : _a.includes("264")) || ((_b = video.codec) == null ? void 0 : _b.includes("265"))) { args.push("-crf", video.quality.toString()); } else { args.push("-q:v", video.quality.toString()); } } if (video.fps) { args.push("-r", video.fps.toString()); } if (video.size) { const size = this.formatSize(video.size); if (size) { args.push("-s", size); } } if (video.aspectRatio) { args.push("-aspect", video.aspectRatio.toString()); } if (video.preset) { args.push("-preset", video.preset); } if (video.profile) { args.push("-profile:v", video.profile); } if (video.level) { args.push("-level", video.level); } if (video.pixelFormat) { args.push("-pix_fmt", video.pixelFormat); } if (video.keyframeInterval) { args.push("-g", video.keyframeInterval.toString()); } if (video.bframes !== void 0) { args.push("-bf", video.bframes.toString()); } if (video.refs !== void 0) { args.push("-refs", video.refs.toString()); } if (video.frames !== void 0) { args.push("-frames:v", video.frames.toString()); } if (video.loop !== void 0) { args.push("-loop", video.loop.toString()); } this.addVideoFilters(args, video); } /** * Add video filters */ static addVideoFilters(args, video) { const filterParts = []; if (video.upscale) { const upscaleFilters = FilterBuilder.buildUpscaleFilter(video.upscale); filterParts.push(...upscaleFilters); } if (video.downscale) { const downscaleFilters = FilterBuilder.buildDownscaleFilter(video.downscale); filterParts.push(...downscaleFilters); } if (video.filters) { const filterString = FilterBuilder.buildVideoFilters(video.filters); if (filterString) { filterParts.push(filterString); } } if (filterParts.length > 0) { args.push("-vf", filterParts.join(",")); } } /** * Add audio-related arguments */ static addAudioArgs(args, audio) { if (audio.disabled) { args.push("-an"); return; } if (audio.codec) { args.push("-c:a", audio.codec); } if (audio.bitrate) { const bitrate = typeof audio.bitrate === "number" ? `${audio.bitrate}` : audio.bitrate; args.push("-b:a", bitrate); } if (audio.quality !== void 0) { args.push("-q:a", audio.quality.toString()); } if (audio.channels) { args.push("-ac", audio.channels.toString()); } if (audio.frequency) { args.push("-ar", audio.frequency.toString()); } if (audio.profile) { args.push("-profile:a", audio.profile); } if (audio.volumeNormalization) { args.push("-af", "loudnorm"); } if (audio.filters) { const filterString = FilterBuilder.buildAudioFilters(audio.filters); if (filterString) { if (audio.volumeNormalization) { const existingAf = args[args.indexOf("-af") + 1]; args[args.indexOf("-af") + 1] = `${existingAf},${filterString}`; } else { args.push("-af", filterString); } } } } /** * Add advanced options */ static addAdvancedOptions(args, options) { if (options.twoPass) { const passlogfile = options.passlogfile || "ffmpeg2pass"; args.push("-pass", "1", "-passlogfile", passlogfile); } if (options.metadata) { Object.entries(options.metadata).forEach(([key, value]) => { args.push("-metadata", `${key}=${value}`); }); } if (options.subtitles) { if (options.burnSubtitles) { args.push("-vf", `subtitles=${options.subtitles}`); } else { args.push("-i", options.subtitles, "-c:s", "mov_text"); } } } /** * Format time value (seconds or timestamp) */ static formatTime(time) { if (typeof time === "string") { return time; } const hours = Math.floor(time / 3600); const minutes = Math.floor(time % 3600 / 60); const seconds = time % 60; return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; } /** * Format size specification */ static formatSize(size) { if (!size) return null; if (typeof size === "string") { return size; } if (typeof size === "object") { const w = size.width === "?" ? "-1" : size.width; const h = size.height === "?" ? "-1" : size.height; return `${w}x${h}`; } return null; } /** * Generate command string for display/logging */ static generateCommandString(config, ffmpegPath = "ffmpeg") { const args = this.generate(config); return `${ffmpegPath} ${args.join(" ")}`; } /** * Validate configuration before generation */ static validate(config) { const errors