fix: dynamic video duration across ProductionForm + LivePreviewCanvas
- Expanded useVideoDurations to detect both form-sourced segments AND editable-slot video fields inside content scenes - Wired videoDurations into ProductionForm (getTemplateDuration, compileExpressToTimeline, LivePreviewCanvas, ExportModal) - LivePreviewCanvas now accepts videoDurations prop - Simplified getTemplateDuration/compiler: any scene with a known video duration uses it, regardless of segmentSource type - A 33s uploaded video now creates a 33s timeline, not 5s
This commit is contained in:
@@ -14,6 +14,7 @@ import { TemplateFieldInput } from '../shared/TemplateFieldInput';
|
||||
import { LivePreviewCanvas } from '../shared/LivePreviewCanvas';
|
||||
import { migrateExpressFields } from '../../context/TemplateBuilderContext';
|
||||
import { useBatchProduction } from '../../hooks/useBatchProduction';
|
||||
import { useVideoDurations } from '../../hooks/useVideoDurations';
|
||||
import { BatchDataPanel } from './BatchDataPanel';
|
||||
import { exportBatchAsZip, BatchExportProgress } from '../../utils/batchExporter';
|
||||
|
||||
@@ -97,16 +98,19 @@ export const ProductionForm: React.FC<ProductionFormProps> = ({
|
||||
|
||||
const playerRef = useRef<BradlyPlayerRef>(null);
|
||||
|
||||
// Probe actual video durations for dynamic timeline
|
||||
const videoDurations = useVideoDurations(template, fieldData);
|
||||
|
||||
const designMD = brand.design;
|
||||
const fps = 30;
|
||||
const totalDuration = getTemplateDuration(template);
|
||||
const totalDuration = getTemplateDuration(template, videoDurations);
|
||||
const totalFrames = Math.max(30, totalDuration * fps);
|
||||
|
||||
// ─── Compile for ExportModal (only when modal is open — LivePreviewCanvas handles its own compile) ───
|
||||
const compiled = useMemo(
|
||||
() => {
|
||||
if (!showExportModal) return { elements: [], layers: [] };
|
||||
const result = compileExpressToTimeline(template, fieldData, designMD, brand);
|
||||
const result = compileExpressToTimeline(template, fieldData, designMD, brand, videoDurations);
|
||||
result.elements = result.elements.map(el => {
|
||||
const fieldId = el.sourceFieldId;
|
||||
const fitOverride = fieldId ? mediaFits[fieldId] : undefined;
|
||||
@@ -121,7 +125,7 @@ export const ProductionForm: React.FC<ProductionFormProps> = ({
|
||||
});
|
||||
return result;
|
||||
},
|
||||
[showExportModal, template, fieldData, designMD, brand, mediaFits, containBgColors]
|
||||
[showExportModal, template, fieldData, designMD, brand, mediaFits, containBgColors, videoDurations]
|
||||
);
|
||||
|
||||
// ─── Collect all TemplateFields across all scenes ───
|
||||
@@ -600,6 +604,7 @@ export const ProductionForm: React.FC<ProductionFormProps> = ({
|
||||
activeSceneId={activeSceneId}
|
||||
onSceneChange={setActiveSceneId}
|
||||
playerRef={playerRef}
|
||||
videoDurations={videoDurations}
|
||||
statusLabel={
|
||||
batch.isBatchMode
|
||||
? (batch.pieceCount > 0 ? `Pieza ${activeBatchPieceIndex + 1} de ${batch.pieceCount}` : 'Sin piezas')
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface LivePreviewCanvasProps {
|
||||
statusLabel?: string;
|
||||
/** Whether all required fields are complete */
|
||||
isComplete?: boolean;
|
||||
/** Video duration overrides per scene (from useVideoDurations) */
|
||||
videoDurations?: Record<string, number>;
|
||||
}
|
||||
|
||||
/** Format frame number to mm:ss */
|
||||
@@ -66,6 +68,7 @@ export const LivePreviewCanvas: React.FC<LivePreviewCanvasProps> = ({
|
||||
playerRef: externalRef,
|
||||
statusLabel,
|
||||
isComplete = false,
|
||||
videoDurations,
|
||||
}) => {
|
||||
const internalRef = useRef<BradlyPlayerRef>(null);
|
||||
const playerRef = externalRef || internalRef;
|
||||
@@ -76,13 +79,13 @@ export const LivePreviewCanvas: React.FC<LivePreviewCanvasProps> = ({
|
||||
const isScrubbing = useRef(false);
|
||||
|
||||
const fps = 30;
|
||||
const totalDuration = getTemplateDuration(template);
|
||||
const totalDuration = getTemplateDuration(template, videoDurations);
|
||||
const totalFrames = Math.max(30, totalDuration * fps);
|
||||
const dimensions = getAspectDimensions(template.aspectRatio);
|
||||
|
||||
// Compile template to timeline (reactive to fieldData + mediaFits)
|
||||
const compiled = useMemo(() => {
|
||||
const result = compileExpressToTimeline(template, fieldData, designMD, brand);
|
||||
const result = compileExpressToTimeline(template, fieldData, designMD, brand, videoDurations);
|
||||
// Strip transitions and apply mediaFit overrides
|
||||
result.elements = result.elements.map(el => {
|
||||
const fieldId = el.sourceFieldId;
|
||||
@@ -97,7 +100,7 @@ export const LivePreviewCanvas: React.FC<LivePreviewCanvasProps> = ({
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}, [template, fieldData, designMD, brand, mediaFits, containBgColors]);
|
||||
}, [template, fieldData, designMD, brand, mediaFits, containBgColors, videoDurations]);
|
||||
|
||||
const playerInputProps = useMemo(() => ({
|
||||
designMD,
|
||||
|
||||
@@ -2,13 +2,18 @@ import { useState, useEffect } from 'react';
|
||||
import { ExpressTemplate } from '../types';
|
||||
|
||||
/**
|
||||
* useVideoDurations — Probes actual video durations for form-sourced scenes.
|
||||
* useVideoDurations — Probes actual video durations for scenes that contain
|
||||
* user-uploaded videos.
|
||||
*
|
||||
* When a user uploads a video to a form field, this hook creates a temporary
|
||||
* <video> element to read its metadata and returns the actual duration in seconds.
|
||||
* Detects two kinds of video sources:
|
||||
* 1. Form-sourced segments (intro/outro with segmentSource='form')
|
||||
* → fieldData key: "segment-{scene.id}"
|
||||
* 2. Editable-slot video fields inside content scenes
|
||||
* → fieldData key: field.id
|
||||
*
|
||||
* Returns a map of sceneId → duration (seconds) for scenes whose uploaded
|
||||
* videos have been probed. Scenes without uploaded videos are not included.
|
||||
* Returns a map of sceneId → duration (seconds). When a video is uploaded,
|
||||
* its actual duration overrides the static scene.durationSeconds in
|
||||
* getTemplateDuration and compileExpressToTimeline.
|
||||
*/
|
||||
export function useVideoDurations(
|
||||
template: ExpressTemplate | null,
|
||||
@@ -19,28 +24,52 @@ export function useVideoDurations(
|
||||
useEffect(() => {
|
||||
if (!template) return;
|
||||
|
||||
// Find scenes that source video from form uploads
|
||||
const formVideoScenes = template.scenes.filter(
|
||||
s => s.segmentSource === 'form' && (s.type === 'intro' || s.type === 'outro' || s.type === 'content')
|
||||
);
|
||||
// Collect all (sceneId, videoUrl) pairs to probe
|
||||
const toProbe: { sceneId: string; videoUrl: string }[] = [];
|
||||
|
||||
for (const scene of formVideoScenes) {
|
||||
for (const scene of template.scenes) {
|
||||
// 1. Form-sourced segments (intro/outro)
|
||||
if (scene.segmentSource === 'form') {
|
||||
const fieldId = `segment-${scene.id}`;
|
||||
const videoUrl = fieldData[fieldId];
|
||||
|
||||
if (!videoUrl || videoUrl.trim() === '') {
|
||||
// No video uploaded — remove any stale duration
|
||||
setDurations(prev => {
|
||||
if (prev[scene.id]) {
|
||||
const { [scene.id]: _, ...rest } = prev;
|
||||
return rest;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
if (videoUrl && videoUrl.trim()) {
|
||||
toProbe.push({ sceneId: scene.id, videoUrl });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Probe the video duration
|
||||
// 2. Editable-slot video fields inside any scene
|
||||
const fields = scene.fields ?? [];
|
||||
for (const field of fields) {
|
||||
if (
|
||||
field.type === 'video' &&
|
||||
field.nature === 'editable-slot'
|
||||
) {
|
||||
const videoUrl = fieldData[field.id];
|
||||
if (videoUrl && videoUrl.trim()) {
|
||||
toProbe.push({ sceneId: scene.id, videoUrl });
|
||||
break; // One video per scene is enough to set the duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear stale durations for scenes that no longer have a video
|
||||
const activeSceneIds = new Set(toProbe.map(p => p.sceneId));
|
||||
setDurations(prev => {
|
||||
const cleaned = { ...prev };
|
||||
let changed = false;
|
||||
for (const key of Object.keys(cleaned)) {
|
||||
if (!activeSceneIds.has(key)) {
|
||||
delete cleaned[key];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? cleaned : prev;
|
||||
});
|
||||
|
||||
// Probe each video
|
||||
for (const { sceneId, videoUrl } of toProbe) {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
|
||||
@@ -48,7 +77,7 @@ export function useVideoDurations(
|
||||
if (video.duration && isFinite(video.duration)) {
|
||||
setDurations(prev => ({
|
||||
...prev,
|
||||
[scene.id]: video.duration,
|
||||
[sceneId]: video.duration,
|
||||
}));
|
||||
}
|
||||
video.remove();
|
||||
|
||||
@@ -82,15 +82,15 @@ export function getAspectDimensions(aspect: string): { w: number; h: number } {
|
||||
* Compute total duration of template in seconds.
|
||||
* @param videoDurations Optional map of scene.id → actual video duration (seconds).
|
||||
* When provided, overrides the static durationSeconds for scenes
|
||||
* that source video from form uploads.
|
||||
* that have user-uploaded video content.
|
||||
*/
|
||||
export function getTemplateDuration(
|
||||
template: ExpressTemplate,
|
||||
videoDurations?: Record<string, number>,
|
||||
): number {
|
||||
return template.scenes.reduce((sum, scene) => {
|
||||
// If this scene has a form-sourced video and we know its actual duration, use it
|
||||
if (videoDurations && scene.segmentSource === 'form' && videoDurations[scene.id]) {
|
||||
// If we know the actual video duration for this scene, use it
|
||||
if (videoDurations && videoDurations[scene.id]) {
|
||||
return sum + videoDurations[scene.id];
|
||||
}
|
||||
return sum + scene.durationSeconds;
|
||||
@@ -121,8 +121,8 @@ export function compileExpressToTimeline(
|
||||
|
||||
// Process each scene sequentially — the template's scenes are the sole source of truth
|
||||
for (const scene of template.scenes) {
|
||||
// Use actual video duration if available for form-sourced scenes
|
||||
const sceneDuration = (videoDurations && scene.segmentSource === 'form' && videoDurations[scene.id])
|
||||
// Use actual video duration if available (from useVideoDurations)
|
||||
const sceneDuration = (videoDurations && videoDurations[scene.id])
|
||||
? videoDurations[scene.id]
|
||||
: scene.durationSeconds;
|
||||
const sceneDurFrames = sceneDuration * fps;
|
||||
|
||||
Reference in New Issue
Block a user