blanchon commited on
Commit
dd360a4
·
1 Parent(s): 6c84e50

Video: drive virtualTime emission off rAF for smooth minimap

Browse files
src/lib/components/grid-tile.svelte CHANGED
@@ -109,11 +109,32 @@
109
  videoEl.playbackRate = playbackRate;
110
  });
111
 
112
- function onTimeUpdate() {
113
- if (videoEl && isLeader) {
114
- onLeaderTime?.(videoEl.currentTime);
115
- }
116
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
  function onEnded() {
119
  if (isLeader) onLeaderEnded?.();
@@ -181,7 +202,6 @@
181
  disableremoteplayback
182
  controlslist="nodownload nofullscreen noplaybackrate noremoteplayback"
183
  oncontextmenu={(e) => e.preventDefault()}
184
- ontimeupdate={onTimeUpdate}
185
  onended={onEnded}
186
  onprogress={onProgress}
187
  onloadeddata={onLoadedData}
 
109
  videoEl.playbackRate = playbackRate;
110
  });
111
 
112
+ // Drive leader-time emission off rAF instead of the <video>'s `timeupdate`
113
+ // event. `timeupdate` fires at ~4 Hz best-case and drops to ~1 Hz under
114
+ // load, which is what was making the minimap stutter. rAF is bound to the
115
+ // display refresh, free when paused (t doesn't change → no emit), and
116
+ // auto-throttled when the tab is backgrounded.
117
+ $effect(() => {
118
+ if (!isLeader || !videoEl) return;
119
+ const el = videoEl;
120
+ let cancelled = false;
121
+ let raf = 0;
122
+ let last = -1;
123
+ const tick = () => {
124
+ if (cancelled) return;
125
+ const t = el.currentTime;
126
+ if (t !== last) {
127
+ last = t;
128
+ onLeaderTime?.(t);
129
+ }
130
+ raf = requestAnimationFrame(tick);
131
+ };
132
+ raf = requestAnimationFrame(tick);
133
+ return () => {
134
+ cancelled = true;
135
+ cancelAnimationFrame(raf);
136
+ };
137
+ });
138
 
139
  function onEnded() {
140
  if (isLeader) onLeaderEnded?.();
 
202
  disableremoteplayback
203
  controlslist="nodownload nofullscreen noplaybackrate noremoteplayback"
204
  oncontextmenu={(e) => e.preventDefault()}
 
205
  onended={onEnded}
206
  onprogress={onProgress}
207
  onloadeddata={onLoadedData}
src/lib/components/video-player/video-player.svelte CHANGED
@@ -83,19 +83,32 @@
83
  onChunkChange?.(activeChunkIdx);
84
  });
85
 
86
- let lastEmitted = -1;
 
 
 
87
  $effect(() => {
88
- const t = currentTime;
89
- // During a chunk swap the new <video> element transiently reports
90
- // currentTime=0 before its loadedmetadata seek; echoing that to the
91
- // parent would snap virtualTime (and therefore the minimap + timeline)
92
- // back to round-start for ~1 frame. Hold off until the new clip's
93
- // data has actually loaded.
94
- if (!isVideoData) return;
95
- if (Math.abs(t - lastEmitted) >= 0.2) {
96
- lastEmitted = t;
97
- onTimeUpdate?.(t);
98
- }
 
 
 
 
 
 
 
 
 
 
99
  });
100
 
101
  function chunksSig(cs: Chunk[]): string {
 
83
  onChunkChange?.(activeChunkIdx);
84
  });
85
 
86
+ // Emit time on every rAF tick rather than on `<video>`'s `timeupdate` event
87
+ // (which fires at ~4 Hz best-case, ~1 Hz under load — visible minimap
88
+ // stutter). `isVideoData` gates the transient currentTime=0 that a fresh
89
+ // chunk's <video> reports before its loadedmetadata seek lands.
90
  $effect(() => {
91
+ if (!videoEl) return;
92
+ const el = videoEl;
93
+ let cancelled = false;
94
+ let raf = 0;
95
+ let last = -1;
96
+ const tick = () => {
97
+ if (cancelled) return;
98
+ if (isVideoData) {
99
+ const t = el.currentTime;
100
+ if (t !== last) {
101
+ last = t;
102
+ onTimeUpdate?.(t);
103
+ }
104
+ }
105
+ raf = requestAnimationFrame(tick);
106
+ };
107
+ raf = requestAnimationFrame(tick);
108
+ return () => {
109
+ cancelled = true;
110
+ cancelAnimationFrame(raf);
111
+ };
112
  });
113
 
114
  function chunksSig(cs: Chunk[]): string {