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