ffmpeg-forge
Version:
A modern, type-safe FFmpeg wrapper for Node.js with zero dependencies
1,591 lines • 144 kB
JavaScript
"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