fix(rendering): synchronize FPS, implement render locks, respect brand segment duration, and fix local audio path resolving for ffmpeg

This commit is contained in:
2026-06-02 20:40:30 -05:00
parent b7656cf8eb
commit 9503dbfabc
39 changed files with 3556 additions and 3506 deletions
+50 -1
View File
@@ -8,6 +8,12 @@ import { spawn } from 'child_process';
import path from 'path';
import fs from 'fs';
export interface AudioTrackInput {
url: string;
startFrame: number;
volume: number; // 0 to 1
}
export interface EncodeOptions {
/** Directory containing numbered frame images */
framesDir: string;
@@ -25,6 +31,10 @@ export interface EncodeOptions {
height?: number;
/** Quality: CRF value (lower = better, 18-28 typical) */
crf?: number;
/** Audio tracks to mix */
audioTracks?: AudioTrackInput[];
/** Total frames (used to force exact video length) */
durationInFrames?: number;
/** Progress callback */
onProgress?: (percent: number) => void;
}
@@ -40,6 +50,8 @@ export async function encodeVideo(options: EncodeOptions): Promise<void> {
fps,
codec,
crf = 23,
audioTracks = [],
durationInFrames,
onProgress,
} = options;
@@ -56,9 +68,14 @@ export async function encodeVideo(options: EncodeOptions): Promise<void> {
const args: string[] = [
'-y', // Overwrite output
'-framerate', String(fps), // Input framerate
'-i', inputPattern, // Input pattern
'-i', inputPattern, // Input 0: Image pattern
];
// Add audio inputs
audioTracks.forEach(track => {
args.push('-i', track.url);
});
// Codec-specific settings
if (codec === 'h264') {
args.push(
@@ -84,6 +101,38 @@ export async function encodeVideo(options: EncodeOptions): Promise<void> {
);
}
// Audio codec
args.push('-c:a', 'aac', '-b:a', '192k');
// Build Audio Filter Complex
if (audioTracks.length > 0) {
const filterParts: string[] = [];
const mixInputs: string[] = [];
audioTracks.forEach((track, index) => {
const inputIndex = index + 1; // 0 is video
const delayMs = Math.round((track.startFrame / fps) * 1000);
const outLabel = `[a${inputIndex}]`;
// adelay applies delay to all channels (L|R)
filterParts.push(`[${inputIndex}:a]adelay=${delayMs}|${delayMs},volume=${track.volume}${outLabel}`);
mixInputs.push(outLabel);
});
// Mix all processed audio streams
const mixFilter = `${mixInputs.join('')}amix=inputs=${audioTracks.length}:duration=longest:dropout_transition=2[aout]`;
filterParts.push(mixFilter);
args.push('-filter_complex', filterParts.join(';'));
args.push('-map', '0:v'); // Map video from input 0
args.push('-map', '[aout]'); // Map audio from complex filter
}
// Force video length
if (durationInFrames) {
args.push('-frames:v', String(Math.round(durationInFrames)));
}
args.push(outputPath);
// Count total frames for progress