Handoff: Mobile cold-load config-hydration wait¶
Date¶
2026-07-13
Summary¶
Fixed a Firefox-for-Android (Fenix) bug where a cold-loaded watch page could never activate audio-only. Mobile YouTube (ytm-app) hydrates window.ytcfg (and its INNERTUBE_API_KEY) after the content script's document_start injection and fires no DOM or navigation event when it does. The first activation would extract an empty key, bail to fallback/not-a-watch-page, and never retry, because on mobile no SPA navigation event follows to re-arm it. Activation now waits, bounded and generation-guarded, for the key to appear before giving up.
No hard invariant changed: logged-out only, credentialless ANDROID_VR, PlayerHandle as the sole <video>.src writer, fail-open to native playback, and the page-world trust boundary all hold. Desktop behavior is unchanged.
Branch rebuild, PR #65.
What changed¶
entrypoints/main-world.ts— newwaitForConfigReady(operationGeneration): a bounded poll (250 ms steps, ceilingVIDEO_WAIT_MS= 8000 ms) forgetConfigString('INNERTUBE_API_KEY').activateEnhancementscalls it only when a watch page has a video id but no key yet (if (videoId && !apiKey)), and re-checks the generation on return so a navigation during the wait aborts cleanly. It resolves early the instant the key appears. On desktop (key already hydrated) and on genuine non-watch pages (no video id) the poll never runs, so no path that already has the key gains any latency.
Verification¶
- New deterministic bench case
m0:cold-config-hydration-activates(tests/e2e/bench/run-bench.mjs,tests/e2e/bench/fixture-server.mjs). The fixture watch page gained acoldConfig=1mode:INNERTUBE_API_KEYis omitted from the inlineytcfgatdocument_startand set ~600 ms later viaytcfg.set(...), reproducing mobile late-hydration. The case asserts the extension waits and reachesactivewith a/videoplaybacksource and anactivebackground-map entry. - Red-green proven. With the wait neutralized the case fails (
fallback/not-a-watch-page, bench 48/49); with the wait in place it passes (bench 49/49). This is the deterministic guard the production fix previously lacked (the fixture had always exposed the key synchronously). - Real Fenix confirmation. A sequential Firefox-for-Android emulator probe (ARM64
aosp_atdAVD, Fenix Nightly) showed 8/8 coldm.youtube.com/watchloads reachingstatus:"active"with the fix, versus the pre-fixnot-a-watch-pagebail. This probe is device-gated and manual, not a CI gate; the bench case above is the reproducible regression lock. - Gate green:
npm run typecheck,npm run lint,npm run format:check,npm test(246 passing, coverage 97.97% st / 96.21% br / 96.29% fn / 99.05% ln), andnpm run test:bench(49/49). CI runs the bench and matrix under xvfb (.github/workflows/ci.yml), so the new case gates on every pipeline run, not only locally.
Known issues / notes¶
- Mobile audio-only still does not hold after activation. Reaching
activeis necessary but not sufficient on Fenix: the native player reclaims the<video>within ~165 ms through a path that bypassesPlayerHandle's.src-property guard entirely (reassertCountstayed 0 across every sampled run; the reclaim uses attribute-levelsetAttribute/removeAttributeplus asrcObjectreset, and ablob:re-appears incurrentSrcvia a surface none of the probes instrumented, which has the signature of the element being replaced). This is a distinct, deeper bug from the cold-load race fixed here, and it is not a "circuit breaker trips too fast" problem (the guard never engages). It is being investigated separately to determine whether the element is replaced (native takeover = the fail-open outcome) or the same element is re-sourced (fixable with an attribute +srcObjectguard). - Mobile HOLD cannot be proven in the headless emulator. The
-no-windowARM64 emulator cannot produce a trusted user gesture (navigator.userActivationstays false; content never gets window focus), so YouTube's own player JS re-pauses any scriptedplay(). The emulator can prove the reclaim mechanism and whether the src holds, but proving audio keeps decoding for a real user needs a real Fenix device (the owner-gated device lane). Perf/memory sampling over ~150 s showed no runaway (Total PSS settled 252 MB -> 226 MB; device-wide CPU 18% burst -> 9.3% steady), but reflects the activation + reclaim cycle, not continuous audio decode, for the same reason.
Next steps¶
- Resolve the mobile reclaim mechanism (element-replacement vs same-element re-source) and either implement a same-element attribute/
srcObjectguard or record element-replacement as the native fallback in an ADR. - Stand up the owner-gated real-Fenix device lane so mobile audio-only HOLD has an automated proof.