Skip to content

Commit b834416

Browse files
committed
fix(preview): hand out no native view while the view for a new source is created
1 parent 507c6d4 commit b834416

2 files changed

Lines changed: 37 additions & 4 deletions

File tree

‎src/native/hooks/useNativeCompositorView.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,4 +291,30 @@ describe("useNativeCompositorView", () => {
291291
expect(mocks.readCompositorFrame).not.toHaveBeenCalled();
292292
expect(result.current.error).toBeNull();
293293
});
294+
295+
// Issue #960: on a project switch the old view is destroyed in the same commit that
296+
// starts creating the new one. Handing out the old id meanwhile let the caller push the
297+
// new project's scene and clip to the dying view, and the preview drew captions and
298+
// frames at the wrong times.
299+
it("hands out no view id while the view for a new source is being created", async () => {
300+
mocks.readCompositorFrame.mockResolvedValue(null);
301+
let resolveSecond: (value: { id: number }) => void = () => undefined;
302+
mocks.createCompositorView
303+
.mockResolvedValueOnce({ id: 7 })
304+
.mockReturnValueOnce(new Promise((resolve) => (resolveSecond = resolve)));
305+
306+
const ref = stubCanvasRef();
307+
const { result, rerender } = renderHook(
308+
({ screenPath }) => useNativeCompositorView(ref, { sources: { screenPath } }),
309+
{ initialProps: { screenPath: "a.mp4" } },
310+
);
311+
await waitFor(() => expect(result.current.viewId).toBe(7));
312+
313+
rerender({ screenPath: "b.mp4" });
314+
expect(result.current.viewId).toBeNull();
315+
expect(mocks.destroyCompositorView).toHaveBeenCalledWith(7);
316+
317+
resolveSecond({ id: 8 });
318+
await waitFor(() => expect(result.current.viewId).toBe(8));
319+
});
294320
});

‎src/native/hooks/useNativeCompositorView.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,19 @@ export function useNativeCompositorView(
9191
// Re-create the native view when the screen source changes (e.g. loading a different
9292
// project) so it never keeps showing a stale clip.
9393
const screenPath = opts.sources?.screenPath;
94-
const [viewId, setViewId] = useState<number | null>(null);
94+
const [view, setView] = useState<{ id: number; screenPath: string | undefined } | null>(null);
95+
// A view is only handed out for the source it was created for. When a project switch
96+
// changes the source, the old view is destroyed in this very commit while the new one is
97+
// still being created; returning its id in the meantime let the caller's effects push the
98+
// NEW project's scene and clip to the dying view, and the preview then drew captions and
99+
// frames at the wrong times until something re-pushed the scene (#960).
100+
const viewId = view && view.screenPath === screenPath ? view.id : null;
95101
const [error, setError] = useState<string | null>(null);
96102
// Mirror into a ref so async callbacks always see the freshest id without
97-
// re-subscribing the main effect.
103+
// re-subscribing the main effect. It tracks the live native view, not `viewId`, so the
104+
// cleanup still destroys the old view after a source change.
98105
const viewIdRef = useRef<number | null>(null);
99-
viewIdRef.current = viewId;
106+
viewIdRef.current = view?.id ?? null;
100107

101108
useEffect(() => {
102109
if (!enabled) {
@@ -300,7 +307,7 @@ export function useNativeCompositorView(
300307
return;
301308
}
302309
viewIdRef.current = result.id;
303-
setViewId(result.id);
310+
setView({ id: result.id, screenPath });
304311
});
305312

306313
const observer = new ResizeObserver(scheduleRectUpdate);

0 commit comments

Comments
 (0)