Skip to content

A deferred caption application can fire on the next media, applying the previous document's captions #283

Description

@maboa

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

  1. Point the player at media whose metadata never arrives (a slow or hanging URL).
  2. Run a caption pass for transcript A — the listener is registered holding A's captions.
  3. Replace the media and apply transcript B's captions.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions