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
Priority
Should land before any release that broadens beyond Relisten's controlled URL set.
Background
PR #19 introduced sample-accurate HTML5 → Web Audio crossfading by routing the
<audio>element through aMediaElementAudioSourceNodeas soon as theAudioContextis 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-Originheader 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. Thetry/catchfallback inTrack.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.machineand 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:
The crossover code already has a fallback path for
_html5GainNode === null, so the ctx-getter branch is the only place to gate.Acceptance criteria
Access-Control-Allow-Originand assertcreateMediaElementSourceis not called and the mastergainNode → destinationconnection is still established.Priority
Should land before any release that broadens beyond Relisten's controlled URL set.