Skip to content

Gate MediaElementAudioSourceNode routing on CORS support to avoid silent non-CORS streams #20

Description

@switz

Background

PR #19 introduced sample-accurate HTML5 → Web Audio crossfading by routing the <audio> element through a MediaElementAudioSourceNode as soon as the AudioContext is created (HYBRID and WEBAUDIO_ONLY modes).

This routing has a CORS constraint that's easy to miss: when the audio source is cross-origin and the response does not carry an Access-Control-Allow-Origin header that permits the page's origin, browsers still happily attach the media element to the Web Audio graph but render it as silence — createMediaElementSource() does not reliably throw. The try/catch fallback in Track.ts's ctx getter therefore doesn't trigger, and the element's native (audible) output is replaced with the silent Web Audio path.

For the library's primary consumer (Relisten), this doesn't bite: archive.org and the relisten-api both send proper CORS headers. But the package is published on npm and the README points users at arbitrary URLs. A consumer pointing it at, say, a misconfigured S3 bucket or a podcast feed without CORS would get silence with no error — the worst possible failure mode (not detectable without listening).

Proposed fix

Detect CORS support during the HEAD probe in fetchDecode.machine and only build the MediaElementSource path when CORS allows it. Falling back to default HTML5 output keeps the track audible — at the cost of the sample-accurate crossfade (the pre-PR-#19 immediate-pause crossover still applies).

Sketch:

// fetchDecode resolveUrl actor (Track.ts):
const res = await fetch(this._trackUrl, { method: 'HEAD', signal });
const aco = res.headers.get('access-control-allow-origin');
this._corsAllowed = aco === '*' || aco === window.location.origin;

// ctx getter:
if (this._corsAllowed && context && !this.gainNode) {
  // build MediaElementSource → html5GainNode → gainNode chain
} else {
  // master gainNode only; HTML5 stays on default output
}

The crossover code already has a fallback path for _html5GainNode === null, so the ctx-getter branch is the only place to gate.

Acceptance criteria

  • Cross-origin URL without CORS headers plays audibly (HTML5 default output) and doesn't get muted at crossover time.
  • Cross-origin URL with CORS headers continues to use the MediaElementSource crossfade path.
  • Same-origin URL works regardless (CORS not relevant).
  • Test: simulate a HEAD response without Access-Control-Allow-Origin and assert createMediaElementSource is not called and the master gainNode → destination connection is still established.

Priority

Should land before any release that broadens beyond Relisten's controlled URL set.

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