fix(web): initialize caption tracks after load#1885
Conversation
| const bindTrackElementLoads = (): void => { | ||
| for (const trackElement of video.querySelectorAll("track")) { | ||
| if (trackLoadHandlers.has(trackElement)) continue; |
There was a problem hiding this comment.
When
handleTrackUpdate fires after a <track> element is removed from the DOM (via MutationObserver or removetrack), syncTracks correctly cleans up the TextTrack from cueTracks, but trackLoadHandlers is never pruned. The detached HTMLTrackElement and its handler closure are held in memory until the returned cleanup function runs. Pruning stale entries in bindTrackElementLoads keeps the Map tidy across the binding's lifetime.
| const bindTrackElementLoads = (): void => { | |
| for (const trackElement of video.querySelectorAll("track")) { | |
| if (trackLoadHandlers.has(trackElement)) continue; | |
| const bindTrackElementLoads = (): void => { | |
| const liveElements = new Set(video.querySelectorAll("track")); | |
| for (const [el, handler] of trackLoadHandlers) { | |
| if (!liveElements.has(el)) { | |
| el.removeEventListener("load", handler); | |
| trackLoadHandlers.delete(el); | |
| } | |
| } | |
| for (const trackElement of liveElements) { | |
| if (trackLoadHandlers.has(trackElement)) continue; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/app/s/[videoId]/_components/caption-tracks.ts
Line: 61-63
Comment:
When `handleTrackUpdate` fires after a `<track>` element is removed from the DOM (via `MutationObserver` or `removetrack`), `syncTracks` correctly cleans up the `TextTrack` from `cueTracks`, but `trackLoadHandlers` is never pruned. The detached `HTMLTrackElement` and its handler closure are held in memory until the returned cleanup function runs. Pruning stale entries in `bindTrackElementLoads` keeps the Map tidy across the binding's lifetime.
```suggestion
const bindTrackElementLoads = (): void => {
const liveElements = new Set(video.querySelectorAll("track"));
for (const [el, handler] of trackLoadHandlers) {
if (!liveElements.has(el)) {
el.removeEventListener("load", handler);
trackLoadHandlers.delete(el);
}
}
for (const trackElement of liveElements) {
if (trackLoadHandlers.has(trackElement)) continue;
```
How can I resolve this? If you propose a fix, please make it concise.| describe("bindCaptionTrackCueText", () => { | ||
| it("initializes caption cues when a track element loads after metadata", () => { | ||
| const textTracks = new FakeTextTrackList(); | ||
| const trackElement = new FakeTrackElement(); | ||
| const video = createVideo(textTracks, [trackElement]); | ||
| const captionTrack = new FakeTextTrack( | ||
| "captions", | ||
| createCueList("Loaded caption"), | ||
| ); | ||
| const cueTexts: string[] = []; | ||
|
|
||
| const cleanup = bindCaptionTrackCueText(video, (text) => { | ||
| cueTexts.push(text); | ||
| }); | ||
|
|
||
| video.dispatch("loadedmetadata"); | ||
| textTracks.addSilently(captionTrack); | ||
| trackElement.readyState = 2; | ||
| trackElement.dispatch("load"); | ||
| captionTrack.dispatch("cuechange"); | ||
|
|
||
| expect(captionTrack.mode).toBe("hidden"); | ||
| expect(cueTexts.at(-1)).toBe("Loaded caption"); | ||
|
|
||
| cleanup(); | ||
| }); |
There was a problem hiding this comment.
Missing synchronous-init test case
The suite only covers the async VTT-load path (track element loads after loadedmetadata). The synchronous path — where <track> elements are already in the DOM with readyState === 2 before bindCaptionTrackCueText is called — is exercised by the immediate handleTrackUpdate() call at the end of setup, but is not tested. A regression here (e.g., the sync syncTracks call being dropped) would go undetected.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/__tests__/unit/caption-tracks.test.ts
Line: 86-111
Comment:
**Missing synchronous-init test case**
The suite only covers the async VTT-load path (track element loads after `loadedmetadata`). The synchronous path — where `<track>` elements are already in the DOM with `readyState === 2` before `bindCaptionTrackCueText` is called — is exercised by the immediate `handleTrackUpdate()` call at the end of setup, but is not tested. A regression here (e.g., the sync `syncTracks` call being dropped) would go undetected.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
<track>load eventsFixes #1773.
Tests
Note: a broad
pnpm --dir apps/web exec tsc --noEmit --pretty false --incremental falserun was attempted, but it stayed silent for several minutes and was stopped to avoid tying up the sweep environment.Greptile Summary
Extracts caption-track binding logic into a shared
bindCaptionTrackCueTexthelper and fixes the async VTT load race by waiting for theloadevent on<track>DOM elements before registeringcuechangelisteners, rather than relying solely onloadedmetadata.caption-tracks.ts— new helper uses aMutationObserver+<track>elementloadlisteners +textTracksevents to keep caption tracks inhiddenmode and deliver cue-text changes; cleanup is comprehensive.CapVideoPlayer.tsx/HLSVideoPlayer.tsx— inline track-setup blocks replaced with a singlebindCaptionTrackCueTextcall; event wiring and cleanup are correctly delegated to the helper.caption-tracks.test.ts— focused unit test validates the async load path for self-hosted VTT.Confidence Score: 4/5
Safe to merge — the refactor is clean, event cleanup is correct in both players, and the core async-load fix addresses the reported issue.
The caption-track helper is well-structured and the two player components are straightforward call-site cleanups. The only concern is that
trackLoadHandlersretains entries for<track>elements removed from the DOM until the binding is torn down, a mild memory retention issue. The test suite covers the motivating bug path but misses the synchronous already-loaded initialization case.apps/web/app/s/[videoId]/_components/caption-tracks.ts — specifically the
bindTrackElementLoadsfunction and its interaction with track removal.Important Files Changed
<track>DOM elements and syncs TextTrack cuechange bindings; staletrackLoadHandlersentries for removed elements are not pruned during the binding's lifetime.bindCaptionTrackCueText; cleanup and event wiring look correct.bindCaptionTrackCueText; the returned cleanup is now returned directly from the effect, which is correct.Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(web): initialize caption tracks afte..." | Re-trigger Greptile