@@ -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