From a21675e5fc70655a6a1981e90e9545abd98e1604 Mon Sep 17 00:00:00 2001 From: "kevinguevaradevia@gmail.com" Date: Tue, 2 Jun 2026 09:48:28 -0500 Subject: [PATCH] =?UTF-8?q?fix:=203=20critical=20bugs=20=E2=80=94=20audio?= =?UTF-8?q?=20corruption,=20blob=20URLs,=20static=20duration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/components/brand/BrandTabMedia.tsx | 26 ++++++-- src/components/express/ExpressEditor.tsx | 10 ++- src/components/express/SceneFieldEditor.tsx | 13 +++- src/components/shared/TemplateFieldInput.tsx | 13 +++- src/electron/main.ts | 11 +++- src/engine/components/MediaAudio.tsx | 38 +++++++++-- src/engine/components/MediaVideo.tsx | 44 ++++++++++--- src/hooks/useVideoDurations.ts | 66 ++++++++++++++++++++ src/utils/expressCompiler.ts | 28 +++++++-- 9 files changed, 212 insertions(+), 37 deletions(-) create mode 100644 src/hooks/useVideoDurations.ts diff --git a/src/components/brand/BrandTabMedia.tsx b/src/components/brand/BrandTabMedia.tsx index 8abd210..88b2fa5 100644 --- a/src/components/brand/BrandTabMedia.tsx +++ b/src/components/brand/BrandTabMedia.tsx @@ -131,9 +131,16 @@ export const BrandTabMedia: React.FC = ({ 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); + } }} /> @@ -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); + } }} /> diff --git a/src/components/express/ExpressEditor.tsx b/src/components/express/ExpressEditor.tsx index 9cafde5..e4e30d6 100644 --- a/src/components/express/ExpressEditor.tsx +++ b/src/components/express/ExpressEditor.tsx @@ -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 = ({ 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); diff --git a/src/components/express/SceneFieldEditor.tsx b/src/components/express/SceneFieldEditor.tsx index 2a712d9..d52a3cd 100644 --- a/src/components/express/SceneFieldEditor.tsx +++ b/src/components/express/SceneFieldEditor.tsx @@ -178,11 +178,18 @@ export const SceneFieldEditor: React.FC = ({ 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); + } } }} /> diff --git a/src/components/shared/TemplateFieldInput.tsx b/src/components/shared/TemplateFieldInput.tsx index d12d356..d146c34 100644 --- a/src/components/shared/TemplateFieldInput.tsx +++ b/src/components/shared/TemplateFieldInput.tsx @@ -169,11 +169,18 @@ export const TemplateFieldInput: React.FC = ({ 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); + } } }} /> diff --git a/src/electron/main.ts b/src/electron/main.ts index 71da5d4..11489d2 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -269,8 +269,15 @@ app.whenReady().then(async () => { return; } - // 4. Set BRADLY_SERVE_URL for the renderer to find the app - process.env.BRADLY_SERVE_URL = `http://127.0.0.1:${expressPort}`; + // 4. Set BRADLY_SERVE_URL for the headless renderer (Puppeteer) + if (process.env.NODE_ENV === 'development') { + // In dev, the React app is served by electron-vite's Vite dev server + const rendererUrl = process.env.ELECTRON_RENDERER_URL || 'http://localhost:5173'; + process.env.BRADLY_SERVE_URL = rendererUrl; + } else { + // In prod, Express serves both API and static renderer files + process.env.BRADLY_SERVE_URL = `http://127.0.0.1:${expressPort}`; + } // 5. Create the main window createWindow(); diff --git a/src/engine/components/MediaAudio.tsx b/src/engine/components/MediaAudio.tsx index 867af03..25c4e6a 100644 --- a/src/engine/components/MediaAudio.tsx +++ b/src/engine/components/MediaAudio.tsx @@ -3,6 +3,9 @@ * * An