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
+45 -1
View File
@@ -13,7 +13,7 @@ import path from 'path';
import fs from 'fs';
import crypto from 'crypto';
import { renderFrames, renderStill } from '../engine/renderer/puppeteerRenderer';
import { encodeVideo, cleanupFrames } from '../engine/renderer/videoEncoder';
import { encodeVideo, cleanupFrames, AudioTrackInput } from '../engine/renderer/videoEncoder';
// ═══ Types ═══
@@ -58,6 +58,7 @@ export interface RenderJobCreateParams {
const MAX_CONCURRENT = 1; // Rendering is CPU-intensive
const RENDERS_DIR = process.env.BRADLY_RENDERS_DIR || path.join(process.cwd(), 'renders');
const UPLOADS_DIR = process.env.BRADLY_UPLOADS_DIR || path.join(process.cwd(), 'uploads');
// Default serve URL for the running app (dev server or built app)
const DEFAULT_SERVE_URL = process.env.BRADLY_SERVE_URL || 'http://localhost:5173';
@@ -236,12 +237,55 @@ async function renderJob(job: RenderJob): Promise<void> {
const codec = job.format === 'webm' ? 'vp8' as const : 'h264' as const;
// Extract audio tracks
const audioTracks: AudioTrackInput[] = [];
// Helper to resolve audio URLs for FFmpeg
const resolveAudioUrl = (url: string | undefined): string | null => {
if (!url || typeof url !== 'string') return null;
if (url.startsWith('/api/media/')) {
const filename = url.replace('/api/media/', '');
return path.join(UPLOADS_DIR, filename);
}
if (url.startsWith('http')) {
return url;
}
return null;
};
// 1. Brand audio
const brandAudioUrl = resolveAudioUrl(job.inputProps.designMD?.brandAudioUrl);
if (brandAudioUrl) {
audioTracks.push({
url: brandAudioUrl,
startFrame: 0,
volume: job.inputProps.designMD?.brandAudioVolume ?? 1,
});
}
// 2. Timeline elements
const elements = job.inputProps.timelineElements || [];
elements.forEach((el: any) => {
if ((el.type === 'audio' || el.type === 'video') && el.content) {
const audioUrl = resolveAudioUrl(el.content);
if (audioUrl) {
audioTracks.push({
url: audioUrl,
startFrame: el.startFrame || 0,
volume: el.volume ?? 1,
});
}
}
});
await encodeVideo({
framesDir,
framePattern: 'frame-%06d.png',
outputPath,
fps: job.fps,
codec,
audioTracks,
durationInFrames: job.durationInFrames,
onProgress: (percent) => {
// Encoding is ~30% of the work
job.progress = 70 + Math.round(percent * 0.29);