Describe the bug
While editing, the preview pane suddenly disappears and is replaced by the empty-state screen ("Add a video to get started" / "Import video"), even though the project, its asset and its clips are all still loaded — the timeline still shows them.
It happens often, without any deliberate action (no new project, no file removed).
Once it happens, the pane never comes back on its own. It only recovers by:
Ctrl+R (reload the renderer), or
- switching to the Rec or Media stage and back to Edit.
Both workarounds are undiscoverable. A user who does not know them sees a project that looks emptied and is likely to conclude the app lost their work — the "Import video" button is the only affordance offered, which is exactly the wrong action.
Expected behavior
- A project that has an asset and clips must never render the "no video / add a video" empty state.
- A preview failure must be recoverable in place (retry), and must be described for what it is ("the video could not be decoded"), not disguised as "you have no video".
- Ideally the failure should not happen at all.
To Reproduce
No deterministic repro yet — it appears during ordinary editing sessions (scrubbing / playing / editing regions) on Windows 11. It is not tied to a specific project file: the same project reloads fine with Ctrl+R.
Root cause analysis
The empty state is rendered by this condition in src/components/ai-edition/Preview.tsx:
{hasProject && hasAsset && previewSources.length > 0 && !allSourcesFailed ? (
<PreviewCanvas ... />
) : (
<EditorEmptyState hasProject={hasProject} />
)}
Four inputs can produce the empty state. Three of them (hasProject, hasAsset, previewSources) are derived from the global project store, which survives both a Preview remount and a stage switch — MediaStage has no effect that reloads the document, so switching to Media and back could not repair them. The only input that a remount can repair is Preview's own component state, failedSourceIds:
const [failedSourceIds, setFailedSourceIds] = useState<string[]>([]);
const allSourcesFailed =
previewSources.length > 0 &&
previewSources.every((source) => failedSourceIds.includes(source.id));
So the reported symptom pins the fault to that latch. The chain is:
-
The hidden <video> in VirtualPreview fires a single error event.
-
onError treats any error as fatal and reports the asset as dead — the error object itself (video.error.code / .message) is discarded, so we currently have no idea what the real failure is:
onError={(e) => {
pendingSeekRef.current = null;
setLoadState("error");
e.currentTarget.pause();
onVideoError?.(activeSource.id);
}}
-
Preview adds the asset id to failedSourceIds. With a single-asset project, "one source dead" == "every source dead" → allSourcesFailed is true.
-
The latch is only cleared when the set of source URLs changes:
const sourceKey = previewSources.map((s) => `${s.id}::${s.src}`).join("|");
useEffect(() => {
if (previousSourceKeyRef.current !== sourceKey) { ...; setFailedSourceIds([]); }
}, [sourceKey]);
During a normal editing session on one recording, that key never changes. There is no other reset path and no retry — the latch is permanent for the lifetime of the component.
-
Because the stage is a conditional render in NewEditorShell (mode === "edit" ? <Preview/> : mode === "media" ? <MediaStage/> : <RecStage/>), leaving Edit unmounts Preview and drops the latch. That is precisely why the two known workarounds work, and why nothing else does.
Two aggravating factors:
- The failing element isn't even the one drawing the picture. Since the move to the native path, the D3D canvas is the sole pixel source and the
<video> is CSS-hidden, used only as decode clock + audio (see the header comment in PreviewCanvas.tsx). A hidden clock element failing tears down a stage it does not render.
- The empty state lies. With a project loaded it shows "Add a video to get started" and only offers "Import video" — no retry, no mention that a decode failed, no way back.
This is the same symptom as the one fixed in 33c811c ("un asset orphelin vidait tout le preview"), via a different path: that fix stopped an orphan asset from being mounted, but left the "one error latches forever" behaviour untouched.
What we don't know yet: why the error event fires in the first place. Because the handler throws the error object away, the trigger is invisible in the wild. Plausible candidates on Windows, in rough order of likelihood:
MEDIA_ERR_DECODE — a transient hardware-decode failure / GPU process restart, made more likely by the native D3D compositor decoding the same file alongside Chromium;
MEDIA_ERR_NETWORK on the file:// URL — the media file momentarily unreadable (AV scan, another process holding it, a rewrite of the recording after capture);
MEDIA_ERR_ABORTED / an empty-src error — a load aborted by a re-render or a seek, i.e. not a real failure at all.
The first fix should therefore be to record the code and message; the rest of the fix is valid regardless of which one it is.
Proposed fix
A. Stop losing the diagnosis (prerequisite, tiny):
in VirtualPreview's onError, read e.currentTarget.error and log { code, message, src, networkState, readyState, currentTime }.
B. Don't treat every error as fatal (root cause, as far as we can control it):
- ignore
MEDIA_ERR_ABORTED and empty-src errors outright — they are teardown artefacts, not dead media;
- for
MEDIA_ERR_NETWORK / MEDIA_ERR_DECODE, retry: video.load() + re-seek to the current position, 2–3 attempts with a short backoff, before reporting the source as failed;
- only declare an asset dead if the file is actually gone (a cheap existence check over the native bridge) — a file that is still on disk should never collapse the stage.
C. Make the failure recoverable and honest (the "acceptable solution", and worth having even if B fixes the trigger):
failedSourceIds must not be a permanent latch: clear it on retry, on window focus / visibilitychange, and on the next user seek;
- when
hasProject && hasAsset is true, render a dedicated error card — "The preview couldn't be decoded" + Retry (and optionally "Reveal file") — instead of EditorEmptyState. EditorEmptyState should be reserved for genuinely empty projects.
D. Bonus: with the native canvas as the pixel source, a clock-element failure could degrade to a paused-but-visible frame rather than blanking the stage at all.
Environment
- OS: Windows 11
- Version:
main (v4 editor shell), reproduced on a dev build
Files involved
src/components/ai-edition/Preview.tsx — empty-state condition, failedSourceIds latch and its single reset path
src/components/ai-edition/VirtualPreview.tsx — <video> onError handler
src/components/ai-edition/NewEditorShell.tsx — stage switch that unmounts Preview (the accidental recovery)
src/components/ai-edition/EditorEmptyState.tsx — the screen shown instead of the preview
Describe the bug
While editing, the preview pane suddenly disappears and is replaced by the empty-state screen ("Add a video to get started" / "Import video"), even though the project, its asset and its clips are all still loaded — the timeline still shows them.
It happens often, without any deliberate action (no new project, no file removed).
Once it happens, the pane never comes back on its own. It only recovers by:
Ctrl+R(reload the renderer), orBoth workarounds are undiscoverable. A user who does not know them sees a project that looks emptied and is likely to conclude the app lost their work — the "Import video" button is the only affordance offered, which is exactly the wrong action.
Expected behavior
To Reproduce
No deterministic repro yet — it appears during ordinary editing sessions (scrubbing / playing / editing regions) on Windows 11. It is not tied to a specific project file: the same project reloads fine with
Ctrl+R.Root cause analysis
The empty state is rendered by this condition in
src/components/ai-edition/Preview.tsx:Four inputs can produce the empty state. Three of them (
hasProject,hasAsset,previewSources) are derived from the global project store, which survives both aPreviewremount and a stage switch —MediaStagehas no effect that reloads the document, so switching to Media and back could not repair them. The only input that a remount can repair isPreview's own component state,failedSourceIds:So the reported symptom pins the fault to that latch. The chain is:
The hidden
<video>inVirtualPreviewfires a singleerrorevent.onErrortreats any error as fatal and reports the asset as dead — the error object itself (video.error.code/.message) is discarded, so we currently have no idea what the real failure is:Previewadds the asset id tofailedSourceIds. With a single-asset project, "one source dead" == "every source dead" →allSourcesFailedis true.The latch is only cleared when the set of source URLs changes:
During a normal editing session on one recording, that key never changes. There is no other reset path and no retry — the latch is permanent for the lifetime of the component.
Because the stage is a conditional render in
NewEditorShell(mode === "edit" ? <Preview/> : mode === "media" ? <MediaStage/> : <RecStage/>), leaving Edit unmountsPreviewand drops the latch. That is precisely why the two known workarounds work, and why nothing else does.Two aggravating factors:
<video>is CSS-hidden, used only as decode clock + audio (see the header comment inPreviewCanvas.tsx). A hidden clock element failing tears down a stage it does not render.This is the same symptom as the one fixed in 33c811c ("un asset orphelin vidait tout le preview"), via a different path: that fix stopped an orphan asset from being mounted, but left the "one error latches forever" behaviour untouched.
What we don't know yet: why the
errorevent fires in the first place. Because the handler throws the error object away, the trigger is invisible in the wild. Plausible candidates on Windows, in rough order of likelihood:MEDIA_ERR_DECODE— a transient hardware-decode failure / GPU process restart, made more likely by the native D3D compositor decoding the same file alongside Chromium;MEDIA_ERR_NETWORKon thefile://URL — the media file momentarily unreadable (AV scan, another process holding it, a rewrite of the recording after capture);MEDIA_ERR_ABORTED/ an empty-srcerror — a load aborted by a re-render or a seek, i.e. not a real failure at all.The first fix should therefore be to record the code and message; the rest of the fix is valid regardless of which one it is.
Proposed fix
A. Stop losing the diagnosis (prerequisite, tiny):
in
VirtualPreview'sonError, reade.currentTarget.errorand log{ code, message, src, networkState, readyState, currentTime }.B. Don't treat every error as fatal (root cause, as far as we can control it):
MEDIA_ERR_ABORTEDand empty-srcerrors outright — they are teardown artefacts, not dead media;MEDIA_ERR_NETWORK/MEDIA_ERR_DECODE, retry:video.load()+ re-seek to the current position, 2–3 attempts with a short backoff, before reporting the source as failed;C. Make the failure recoverable and honest (the "acceptable solution", and worth having even if B fixes the trigger):
failedSourceIdsmust not be a permanent latch: clear it on retry, on window focus /visibilitychange, and on the next user seek;hasProject && hasAssetis true, render a dedicated error card — "The preview couldn't be decoded" + Retry (and optionally "Reveal file") — instead ofEditorEmptyState.EditorEmptyStateshould be reserved for genuinely empty projects.D. Bonus: with the native canvas as the pixel source, a clock-element failure could degrade to a paused-but-visible frame rather than blanking the stage at all.
Environment
main(v4 editor shell), reproduced on a dev buildFiles involved
src/components/ai-edition/Preview.tsx— empty-state condition,failedSourceIdslatch and its single reset pathsrc/components/ai-edition/VirtualPreview.tsx—<video>onErrorhandlersrc/components/ai-edition/NewEditorShell.tsx— stage switch that unmountsPreview(the accidental recovery)src/components/ai-edition/EditorEmptyState.tsx— the screen shown instead of the preview