fix(rendering): synchronize FPS, implement render locks, respect brand segment duration, and fix local audio path resolving for ffmpeg
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user