@@ -42,9 +42,29 @@ import {
4242 shouldMutePreviewAudio ,
4343} from "../lib/timelineIframeHelpers" ;
4444import { scrubMusicAtSeek , stopScrubPreviewAudio } from "../lib/playbackScrub" ;
45- import { probeMediaUrl , getCachedProbe } from "../lib/mediaProbe" ;
45+ import { applyCachedSourceDurations , probeMissingSourceDurations } from "../lib/mediaProbe" ;
4646import { shouldResumeForwardPlaybackAfterSeek , shouldStopAfterSeek } from "../lib/playbackSeek" ;
4747
48+ /**
49+ * Whether the derived elements differ from the current ones in any field that
50+ * affects rendering (identity, timing, track, or source length) — used to skip
51+ * redundant store writes.
52+ */
53+ function timelineElementsChanged ( prev : TimelineElement [ ] , next : TimelineElement [ ] ) : boolean {
54+ if ( next . length !== prev . length ) return true ;
55+ return next . some ( ( el , i ) => {
56+ const p = prev [ i ] ;
57+ return (
58+ ! p ||
59+ el . id !== p . id ||
60+ el . start !== p . start ||
61+ el . duration !== p . duration ||
62+ el . track !== p . track ||
63+ el . sourceDuration !== p . sourceDuration
64+ ) ;
65+ } ) ;
66+ }
67+
4868export function useTimelinePlayer ( ) {
4969 const iframeRef = useRef < HTMLIFrameElement | null > ( null ) ;
5070 const rafRef = useRef < number > ( 0 ) ;
@@ -66,27 +86,19 @@ export function useTimelinePlayer() {
6686 ( elements : TimelineElement [ ] , nextDuration ?: number ) => {
6787 const state = usePlayerStore . getState ( ) ;
6888 const resolvedDuration = nextDuration ?? state . duration ;
69- const mergedElements = mergeTimelineElementsPreservingDowngrades (
70- state . elements ,
71- elements ,
72- state . duration ,
73- resolvedDuration ,
89+ // applyCachedSourceDurations re-applies the cached probe duration: re-derived
90+ // elements (e.g. after a clip move) can arrive without sourceDuration, which
91+ // otherwise makes trimmed waveforms lose their window.
92+ const mergedElements = applyCachedSourceDurations (
93+ mergeTimelineElementsPreservingDowngrades (
94+ state . elements ,
95+ elements ,
96+ state . duration ,
97+ resolvedDuration ,
98+ ) ,
7499 ) ;
75100
76- const elementsChanged =
77- mergedElements . length !== state . elements . length ||
78- mergedElements . some ( ( el , i ) => {
79- const prev = state . elements [ i ] ;
80- return (
81- ! prev ||
82- el . id !== prev . id ||
83- el . start !== prev . start ||
84- el . duration !== prev . duration ||
85- el . track !== prev . track
86- ) ;
87- } ) ;
88-
89- if ( elementsChanged ) {
101+ if ( timelineElementsChanged ( state . elements , mergedElements ) ) {
90102 setElements ( mergedElements ) ;
91103 }
92104 if (
@@ -100,31 +112,17 @@ export function useTimelinePlayer() {
100112 setTimelineReady ( true ) ;
101113 }
102114
103- // Asynchronously enrich media elements missing sourceDuration via mediabunny.
104- // The probe reads file headers only — no full decode — so this is cheap.
105- const needsProbe = mergedElements . filter (
106- ( el ) =>
107- el . src &&
108- el . sourceDuration == null &&
109- [ "video" , "audio" ] . includes ( el . tag . toLowerCase ( ) ) &&
110- ! getCachedProbe ( el . src ) ,
111- ) ;
112- if ( needsProbe . length > 0 ) {
113- void Promise . allSettled (
114- needsProbe . map ( async ( el ) => {
115- const result = await probeMediaUrl ( el . src ! ) ;
116- if ( ! result ) return ;
117- const key = el . key ?? el . id ;
118- usePlayerStore . setState ( ( state ) => {
119- const idx = state . elements . findIndex ( ( e ) => ( e . key ?? e . id ) === key ) ;
120- if ( idx === - 1 || state . elements [ idx ] . sourceDuration != null ) return { } ;
121- const patched = state . elements . slice ( ) ;
122- patched [ idx ] = { ...state . elements [ idx ] , sourceDuration : result . duration } ;
123- return { elements : patched } ;
124- } ) ;
125- } ) ,
126- ) ;
127- }
115+ // Asynchronously enrich media elements still missing sourceDuration
116+ // (header-only probe, cheap), applying each resolved value to the store.
117+ void probeMissingSourceDurations ( mergedElements , ( key , durationSeconds ) => {
118+ usePlayerStore . setState ( ( state ) => {
119+ const idx = state . elements . findIndex ( ( e ) => ( e . key ?? e . id ) === key ) ;
120+ if ( idx === - 1 || state . elements [ idx ] . sourceDuration != null ) return { } ;
121+ const patched = state . elements . slice ( ) ;
122+ patched [ idx ] = { ...state . elements [ idx ] , sourceDuration : durationSeconds } ;
123+ return { elements : patched } ;
124+ } ) ;
125+ } ) ;
128126 } ,
129127 [ setElements , setTimelineReady , setDuration ] ,
130128 ) ;
0 commit comments