fix: 3 critical bugs — audio corruption, blob URLs, static duration

1. Audio/Video sync: During playback, let the browser handle media naturally
   and only force-seek when drift > 250ms. Prevents audio glitching caused
   by setting currentTime 30x/sec. When paused/scrubbing, sync precisely.

2. Blob URLs → Server uploads: All media uploads (BrandTabMedia, SceneFieldEditor,
   TemplateFieldInput) now POST to /api/upload and use persistent server URLs
   instead of blob: URLs that vanish on refresh.

3. Dynamic duration: Added useVideoDurations hook that probes actual video
   durations from uploaded form videos. getTemplateDuration and
   compileExpressToTimeline now accept videoDurations to override static
   durationSeconds for form-sourced scenes.
This commit is contained in:
2026-06-02 09:48:28 -05:00
parent 32505f95ce
commit a21675e5fc
9 changed files with 212 additions and 37 deletions
+20 -6
View File
@@ -131,9 +131,16 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps> = ({ designMD, handleDe
compact
accept="audio/*"
label="Subir audio"
onFiles={(files) => {
const url = URL.createObjectURL(files[0]);
handleDesignChange('brandAudioUrl', url);
onFiles={async (files) => {
const formData = new FormData();
formData.append('file', files[0]);
try {
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await res.json();
if (data.url) handleDesignChange('brandAudioUrl', data.url);
} catch (err) {
console.error('Audio upload failed:', err);
}
}}
/>
</div>
@@ -231,9 +238,16 @@ const VideoUploadSimple: React.FC<{
compact
accept="video/*"
label="Subir archivo"
onFiles={(files) => {
const url = URL.createObjectURL(files[0]);
onUrlChange(url);
onFiles={async (files) => {
const formData = new FormData();
formData.append('file', files[0]);
try {
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await res.json();
if (data.url) onUrlChange(data.url);
} catch (err) {
console.error('Video upload failed:', err);
}
}}
/>
</div>
+7 -3
View File
@@ -8,6 +8,7 @@ import { StoryboardView } from './StoryboardView';
import { SceneFieldEditor } from './SceneFieldEditor';
import { ExpressStylePanel } from './ExpressStylePanel';
import { compileExpressToTimeline, getAspectDimensions, getTemplateDuration } from '../../utils/expressCompiler';
import { useVideoDurations } from '../../hooks/useVideoDurations';
interface ExpressEditorProps {
designMD: DesignMD;
@@ -62,13 +63,16 @@ export const ExpressEditor: React.FC<ExpressEditorProps> = ({
setFieldData(prev => ({ ...prev, [fieldId]: value }));
}, []);
// Probe actual video durations for form-sourced scenes
const videoDurations = useVideoDurations(selectedTemplate, fieldData);
// Compile template to timeline
const compiled = useMemo(() => {
if (!selectedTemplate) return null;
return compileExpressToTimeline(selectedTemplate, fieldData, designMD, company);
}, [selectedTemplate, fieldData, designMD, company]);
return compileExpressToTimeline(selectedTemplate, fieldData, designMD, company, videoDurations);
}, [selectedTemplate, fieldData, designMD, company, videoDurations]);
const totalDuration = selectedTemplate ? getTemplateDuration(selectedTemplate) : 0;
const totalDuration = selectedTemplate ? getTemplateDuration(selectedTemplate, videoDurations) : 0;
const fps = 30;
const totalFrames = Math.max(30, totalDuration * fps);
+10 -3
View File
@@ -178,11 +178,18 @@ export const SceneFieldEditor: React.FC<SceneFieldEditorProps> = ({
type="file"
accept="image/*,video/*"
className="hidden"
onChange={(e) => {
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
onFieldChange(field.id, url);
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await res.json();
if (data.url) onFieldChange(field.id, data.url);
} catch (err) {
console.error('Media upload failed:', err);
}
}
}}
/>
+10 -3
View File
@@ -169,11 +169,18 @@ export const TemplateFieldInput: React.FC<TemplateFieldInputProps> = ({
type="file"
accept={isVideoField ? 'video/*' : 'image/*'}
className="hidden"
onChange={(e) => {
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
onChange(url);
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await res.json();
if (data.url) onChange(data.url);
} catch (err) {
console.error('Media upload failed:', err);
}
}
}}
/>