Summary
In js/caption.js (v2.6.4), when the media hasn't loaded metadata yet, caption application is deferred to a loadedmetadata listener that closes over the current captionsVtt. If the media is replaced before that event ever fires, the listener stays pending and runs on the new media's loadedmetadata — applying the previous document's captions to it.
The code
// If the media's metadata has already loaded, 'loadedmetadata' will not
// fire again, so apply the captions now; otherwise apply on a single-use
// listener. This prevents a listener persisting with a stale captionsVtt
// closure and re-applying it when a different media subsequently loads.
if (video.readyState >= 1 /* HAVE_METADATA */) {
applyCaptions();
} else {
video.addEventListener('loadedmetadata', function listener() {
applyCaptions();
video.removeEventListener('loadedmetadata', listener, true);
}, true);
}
The comment states the intent exactly, but the removal happens after the listener fires, so it only prevents a second application. It doesn't prevent the first one landing on the wrong media.
Reproduction
- Point the player at media whose metadata never arrives (a slow or hanging URL).
- Run a caption pass for transcript A — the listener is registered holding A's captions.
- Replace the media and apply transcript B's captions.
- When B's media reaches
loadedmetadata, the pending listener fires and overwrites them.
Measured in Chromium — A's captions on B's media, after B had been applied correctly:
hasALPHA (document A's captions): true
hasBRAVO (document B's captions): false
This is reachable in any host that loads remote media at startup and then swaps in a different document — in the Hyperaudio Lite Editor it happens on every boot where a saved project is restored while the intro clip is still loading, and the intro's captions end up on the restored project's video.
Suggested fix
Capture the media identity at registration and refuse to apply if it has changed:
const srcAtRegistration = video.currentSrc || video.src;
video.addEventListener('loadedmetadata', function listener() {
video.removeEventListener('loadedmetadata', listener, true); // clean up either way
if ((video.currentSrc || video.src) !== srcAtRegistration) return;
applyCaptions();
}, true);
Removing the listener first means it's cleaned up on both branches; the guard drops the application when the element is no longer playing what those captions were computed for.
The same expression is used on both sides deliberately — currentSrc can still be empty at registration if resource selection hasn't chosen a source yet, so it falls back to src.
An alternative, if identity comparison seems brittle: cancel the pending listener on loadstart or emptied, both of which fire when new media is loaded into the element.
Context
Worked around downstream in the editor for now (hyperaudio-lite-editor#499) by re-asserting the intended captions after any straggler, but that's a patch over this rather than a fix — the vendored copy here is unmodified.
Summary
In
js/caption.js(v2.6.4), when the media hasn't loaded metadata yet, caption application is deferred to aloadedmetadatalistener that closes over the currentcaptionsVtt. If the media is replaced before that event ever fires, the listener stays pending and runs on the new media'sloadedmetadata— applying the previous document's captions to it.The code
The comment states the intent exactly, but the removal happens after the listener fires, so it only prevents a second application. It doesn't prevent the first one landing on the wrong media.
Reproduction
loadedmetadata, the pending listener fires and overwrites them.Measured in Chromium — A's captions on B's media, after B had been applied correctly:
This is reachable in any host that loads remote media at startup and then swaps in a different document — in the Hyperaudio Lite Editor it happens on every boot where a saved project is restored while the intro clip is still loading, and the intro's captions end up on the restored project's video.
Suggested fix
Capture the media identity at registration and refuse to apply if it has changed:
Removing the listener first means it's cleaned up on both branches; the guard drops the application when the element is no longer playing what those captions were computed for.
The same expression is used on both sides deliberately —
currentSrccan still be empty at registration if resource selection hasn't chosen a source yet, so it falls back tosrc.An alternative, if identity comparison seems brittle: cancel the pending listener on
loadstartoremptied, both of which fire when new media is loaded into the element.Context
Worked around downstream in the editor for now (hyperaudio-lite-editor#499) by re-asserting the intended captions after any straggler, but that's a patch over this rather than a fix — the vendored copy here is unmodified.