Repository navigation
fix(core): route media through Web Audio only when the document can actually read it - #3937
Conversation
…ctually read it isCorsSilenced() compared a media URL's origin against window.location.origin, which stays URL-shaped even inside a sandboxed iframe without allow-same-origin. Inside such a frame the document's actual security origin (window.origin) is the string "null", so same-URL-origin media without a crossorigin attribute was waved through createMediaElementSource, and per the Web Audio spec's MediaElementAudioSourceNode security section that node outputs silence without throwing. The composition kept playing with the timeline advancing and no audio at all. Compare against window.origin instead, treating "null" as never same-origin, so an opaque document falls back to the existing decode-only path unless the media element opts in with crossorigin.
jrusso1020
left a comment
There was a problem hiding this comment.
APPROVE at 5e4625de on the code. The root cause is identified correctly, the fix is at the right chokepoint, and the tests are a genuine divergent pair rather than a restatement of the fix. One should-fix before this leaves draft: the diagnostic this module exists to emit gives dead-end advice in exactly the case the PR adds.
Verified independently
- The mechanism.
isCorsSilenced(webAudioRoute.ts:151-153) now readswindow.origin— the document's security origin — withlocation.originonly as a fallback for a host that doesn't expose it as a string. Treating"null"as never-same-origin is right: an opaque origin is same-origin with nothing, so the shortcut at:152must not fire. - No regression for ordinary documents.
window.originonly reads"null"for an opaque origin. On a normal http(s) page it equalslocation.origin, so the:152shortcut behaves exactly as before. The widening is confined to documents that were already broken. - A CORS opt-in still rescues the opaque case.
:153still defers tohasCorsOptIn, so an element withcrossoriginin a sandboxed document keepsweb-audio. Correct — an opaque document sendsOrigin: null, which a server answeringAccess-Control-Allow-Origin: nullor*will satisfy. file:is untouched. Afile:page's media URL exits at the:146protocol guard before any origin comparison, so the boundary the docstring describes at:131-136still holds.- The chokepoint claim holds.
isCorsSilencedis module-private — the only reference anywhere is this file. External consumers all enter throughclassifyWebAudioMediaRoute:runtime/init.ts,runtime/webAudioTransport.ts, andstudio/src/components/sidebar/AudioRow.tsx. (Body says "five callers"; that's five files including this module and its test. The substance — one private predicate, uniform application — is right.)AudioRow.tsxruns in the main studio document, so it sees no behavior change. - The test helper is sound.
withSelfOrigincaptures the descriptor, redefines withconfigurable: true, and restores in afinally— including thedeletebranch for whenoriginwas inherited rather than an own property. No cross-test leakage.
Should-fix: the diagnostic's remediation line is a no-op in the new case
reportWebAudioMediaRoute is the reason this module is a standalone classifier at all — the docstring at :18 says the whole complaint in #3458 was that nothing was said. On the path this PR adds, something is now said, and it points the wrong way.
The new test's own fixture is ${SAME_ORIGIN}/assets/vo.mp3 — an asset that is same-origin by URL. It now classifies decode-only, so reportWebAudioMediaRoute fires and emits:
details.note(:266): "cross-origin media without acrossoriginopt-in is silent…"lostNote(:277-280): "…— proxy or download the asset to a same-origin URL to keep it."
That last sentence is the one a human acts on, and in an opaque document it cannot work: the asset is already same-origin, and no amount of proxying makes anything same-origin with an opaque origin. The two remediations that would actually work — allow-same-origin on the sandbox, or crossorigin on the element plus Access-Control-Allow-Origin: null/* from the server — are never mentioned. It lands precisely on the compositions that matter, too: lostNote is only appended when nativeUnexpressibleProcessing is non-empty, i.e. fx-chain / automation / audio-group / above-unity-gain.
To be fair to the reason token itself: cross_origin_no_cors is defensible, since from an opaque document every http(s) origin genuinely is foreign. It's the prose remediation that misleads.
Either shape works — a second reason ("opaque_document_origin") carrying its own note, or branching lostNote on whether url.origin matched location.origin. The CLI contract is safe either way: packages/cli/src/utils/checkBrowser.ts:309 matches text.startsWith("[hyperframes] runtime_web_audio_bypass") — the code prefix only, never the reason — so adding a reason value can't break the scraper. Worth doing here rather than as follow-up, because the new test currently pins the misleading reason as expected output, which will ossify it.
Status, not findings
- Still
isDraft: true, so nothing merges on this approval regardless. regression-shards (shard-3 … audio-mux-parity …)has since finished green (8m22s) — that's the shard worth caring about for an audio-routing change. Shards 4/5/7/8 andTests on windows-latest: studio-engine-cliwere still pending when I looked; none of them cover audio mux.
— Rames
|
Sharpening one line in my review: I offered the second- The reason it matters is the thing that makes the prose wrong in the first place: the new opaque test asserts Still safe for the CLI either way, and now checked rather than assumed: (Credit to Somu's read in the Lane C thread for the push on this one — I'd left the two options as a coin flip.) — Rames |
…back (#3940) The opaque-origin fix in #3937 routed correctly but sent the diagnostic down a dead end. An opaque document reached the same `cross_origin_no_cors` verdict a genuinely foreign asset gets, so the message told the author to "proxy or download the asset to a same-origin URL". That is impossible advice, because nothing is same-origin with an opaque origin and the asset is usually same-origin by URL already. Split the verdict into two reasons and give each its own note and remedy. The opaque case now names the two fixes that work: `allow-same-origin` on the sandbox, or `crossorigin` plus an `Access-Control-Allow-Origin` of `null` or `*`. Opacity wins when both conditions hold, since it is the blocking one and the one an author cannot fix by moving the asset. The CLI scraper matches on the code prefix only (checkBrowser.ts:309), so the new reason value does not touch that contract. - Rames
Summary
isCorsSilenced()in@hyperframes/core's Web Audio routing decides whether a media element would go silent if captured throughcreateMediaElementSource. It compared the media URL's origin againstwindow.location.origin, which stays URL-shaped even when the document's actual security origin is opaque (a sandboxed iframe withoutallow-same-origin). Inside such a frame,window.originreads the string"null", butlocation.originstill reports the parent-looking URL — so same-URL-origin media without acrossoriginattribute was waved throughcreateMediaElementSource, and per the Web Audio spec'sMediaElementAudioSourceNodesecurity section that node outputs silence without throwing. The composition kept playing — timeline advancing, visuals animating — with no audio at all.Fix
Compare against
window.origin(the document's real security origin) instead, treating the opaque value"null"as never same-origin with anything. Falls back tolocation.originonly if a host doesn't exposewindow.originas a string at all, so behavior on every real browser is unchanged except for the opaque-document case this fixes.Root cause only — no caller-side workaround. All five callers of
classifyWebAudioMediaRoute/isCorsSilencedroute through this one function, so the fix applies uniformly.Test plan
webAudioRoute.test.tsstub the document's security origin (window.origin) independently oflocation.origin, matching how a sandboxed iframe actually diverges. One asserts the opaque case now falls back to the decode-only path; one asserts a genuinely same-origin document keeps today'sweb-audioresult.packages/corevitest suite: 132 files / 2774 tests passing.packages/corelint (oxlint) and both typecheck targets (tsc --noEmit,typecheck:runtime) clean.Pre-review pass
An independent reviewer (separate from the author) audited this diff before it was opened: read the full file plus every external call site, verified the new tests are non-vacuous by reverting the fix and re-running, checked the diff for absent-as-ok fallback patterns, and confirmed no comment overclaims. One should-fix finding (a host where
window.originisundefinedwould have widened the fallback) was applied before this PR was opened.isCorsSilenceddiff and surrounding functionclassifyWebAudioMediaRouteallow-same-originreportswindow.origin === "null"— spec-documented, not exercised live in this reviewwindow.originis unsupported/undefined(now guarded, not covered by a dedicated test)