Architecture Documentation¶
M0 Architecture¶
YouTube Audio is a Firefox-first WebExtension built from one strict-TypeScript source tree. Firefox Manifest V2 is the shipping target; Firefox Manifest V3 is emitted as a capability artifact.
flowchart LR
subgraph Page[YouTube page]
MAIN[MAIN-world entrypoint]
Player[YouTube player]
end
subgraph Extension[YouTube Audio]
Content[Isolated content entrypoint]
Background[Persistent MV2 background]
Shared[Framework-free shared modules]
Popup[Preact popup]
Options[Preact options]
Storage[(Extension storage)]
end
Content -->|WXT injectScript| MAIN
MAIN -. future typed bridge .-> Content
MAIN -. M1 PlayerHandle .-> Player
Content --> Shared
Background --> Shared
Popup --> Shared
Options --> Shared
Popup <--> Storage
Options <--> Storage
Background <--> Storage Layer Responsibilities¶
Background¶
The persistent MV2 background entrypoint owns privileged APIs, network adapters, downloads, and remote-service proxies. M2a installs an allowlist-based blocking webRequest.onBeforeRequest listener for first-party YouTube telemetry. Its conservative default preserves InnerTube player, attestation, Googlevideo media, log_event, and watch-history endpoints; errors fail open.
Isolated content¶
The content entrypoint runs at document_start on the four supported YouTube match patterns. It injects the unlisted MAIN-world bundle and will later own validated cross-world messaging and DOM-facing features.
MAIN world¶
The MAIN-world entrypoint is the only layer intended to touch YouTube player APIs and page-owned prototypes. M0 installs no hooks. M1 will implement the proven <video>.src hijack behind PlayerHandle.
Shared modules¶
src/shared/ contains framework-neutral contracts and pure logic. Feature modules are inert stubs in M0. The real ANDROID_VR request-body builder is implemented here and tested directly. platform.ts exposes manifest/background capability flags so later network interception and lifecycle logic can stay behind adapters.
UI¶
Popup and options are extension-owned documents built with Preact and @preact/signals. They share one storage-backed settings model and reusable tokenized control kit. The desktop popup is a focused quick-control surface; the responsive options page repeats those quick controls first for Firefox Android, then exposes every setting through searchable groups and progressive disclosure. A separate local flag records the one-time onboarding panel without changing feature settings. Preact is not used in background, content, or page-world code.
State Flow¶
sequenceDiagram
participant User
participant UI as Popup / Options
participant Signal as enabledSignal
participant Store as browser.storage.local
participant Context as Other extension contexts
User->>UI: Toggle protection
UI->>Signal: Optimistic update
UI->>Store: Persist setting
Store-->>Context: storage.onChanged
Context->>Signal: Synchronize value M2a Telemetry Flow¶
sequenceDiagram
participant Page as YouTube page
participant WebRequest as Firefox webRequest
participant Policy as Telemetry policy
participant Network as YouTube endpoint
Page->>WebRequest: First-party request
WebRequest->>Policy: URL + current mode
alt Enumerated telemetry endpoint
Policy-->>WebRequest: Block
WebRequest--xNetwork: Cancel before transmission
else Core, media, log_event, or unknown
Policy-->>WebRequest: Allow
WebRequest->>Network: Request continues unchanged
end Security Boundaries¶
- Page-world data is untrusted. No page-message handler exists in M0.
- The background never accepts arbitrary URLs.
- Only YouTube page patterns and
*.googlevideo.comare granted. - SponsorBlock access is limited to
https://sponsor.ajay.app/*; LRCLIB remains ungranted until its feature lands. - Feature failures must leave native YouTube behavior intact.
M1 Playback Flow¶
The isolated content layer owns extension storage and sends a fixed boolean settings payload to MAIN world through a namespaced, same-origin postMessage. MAIN world owns the credentialless ANDROID_VR fetch, playability gate, SPA generation, visibility override, and PlayerHandle. PlayerHandle is the sole extension writer to the page video source. Only bounded status codes return to the isolated layer; signed media URLs and player responses remain in MAIN world.
sequenceDiagram
participant Storage as Extension storage
participant Content as Isolated content
participant Main as MAIN world
participant API as InnerTube
participant Video as YouTube video
Storage-->>Content: instant settings update
Content->>Main: booleans-only CustomEvent
Main->>API: credentialless ANDROID_VR request
API-->>Main: player response
Main->>Main: playability + direct-audio gate
Main->>Video: PlayerHandle sets audio src
Main-->>Content: bounded status code Failures and unsupported videos fail open to native playback. SPA navigation invalidates stale asynchronous operations before they can attach media. On every teardown (global disable, navigate, re-attach, or circuit breaker) PlayerHandle never rewrites <video>.src; the captured native blob: source is backed by a discarded MediaSource and reassigning it would stall the element. A MAIN-world coordinator instead reclaims native playback in place through YouTube's own player API (#movie_player.loadVideoById) at the live position, pinned to the hijacked videoId and guarded by the element still holding the owned URL. The reclaim is one-shot and fail-open.
M3a Segment-skip Flow¶
The persistent background hashes each video ID and requests only the four-character SHA-256 prefix from SponsorBlock with credentials omitted and no referrer. It filters the anonymity bucket locally and returns normalized, merged ranges through the isolated content bridge. MAIN world listens on the same <video> used by PlayerHandle, seeks to a range end at most once per navigation, and discards stale work after SPA navigation. No view-count, submission, voting, or plaintext-video-ID endpoint exists.
sequenceDiagram
participant Main as MAIN world
participant Content as Isolated content
participant Background as Background
participant Sponsor as SponsorBlock API
participant Video as Shared page video
Main->>Content: nonce-authenticated video ID + categories
Content->>Background: fixed Sponsor segment message
Background->>Background: SHA-256(video ID), first 4 hex
Background->>Sponsor: GET /api/skipSegments/prefix (credentials omitted)
Sponsor-->>Background: anonymity bucket
Background->>Background: exact local filter + merge
Background-->>Main: normalized skip ranges
Video-->>Main: timeupdate
Main->>Video: seek once to segment end Any hashing, network, parsing, bridge, media, or seek failure returns an empty list or no-op and leaves native playback intact.
M3b Quality-of-Life Flow¶
The isolated content layer turns the three cosmetic settings into one extension-managed stylesheet. It replaces the style text on instant storage changes and removes it when globally disabled, without exposing settings through persistent page attributes. The MAIN-world layer feature-detects the native player API for bounded quality-cap reassertion and uses YouTube's own autonav toggle when autoplay-next suppression is enabled.
sequenceDiagram
participant Storage as Extension storage
participant Content as Isolated content
participant Main as MAIN world
participant Player as YouTube player
participant DOM as YouTube page
Storage-->>Content: QoL settings update
Content->>DOM: Replace one cosmetic stylesheet
Content->>Main: Nonce-authenticated settings payload
Main->>Player: Optional quality range + quality hint
Main->>Player: Optional native autonav toggle click
Player-->>Main: Quality-change event
Main->>Player: Bounded quality reassertion Missing selectors, controls, and undocumented player methods are no-ops. No perpetual interval, page prototype patch, DOM deletion, or remote input is used.
M2b Ad-block Flow¶
The persistent MV2 background owns deterministic response rewriting. For enabled /youtubei/v1/player and /youtubei/v1/next POSTs, Firefox filterResponseData buffers the original bytes, removes only the bundled allowlist of ad descriptor keys, and emits the rewritten JSON. Any stream, decoding, parsing, or serialization failure emits the original bytes unchanged. Disabling global protection or ad blocking leaves the response filter inert.
sequenceDiagram
participant Page as YouTube player
participant Filter as Firefox response filter
participant Pruner as Pure ad pruner
participant API as InnerTube
Page->>API: POST player / next
API-->>Filter: response bytes
Filter->>Pruner: buffered UTF-8 JSON
alt Valid response with known ad keys
Pruner-->>Filter: JSON without ad descriptors
Filter-->>Page: rewritten bytes
else Disabled or any failure
Filter-->>Page: original bytes unchanged
end The MAIN-world entrypoint separately applies a small static operation baseline from rescue.ts. Its inline-response operation installs a reversible ytInitialPlayerResponse accessor and JSON.parse wrapper, pruning only parsed player responses that contain known ad keys. The dispatcher accepts only compiled operation IDs, catches failures per operation, and supports cleanup on instant settings changes. A best-effort native-function heuristic skips those hooks when another page-context blocker appears to have wrapped JSON parsing or serialization. The heuristic cannot reliably identify a particular extension because browser extension worlds are isolated. No rescue configuration or code is fetched remotely; that work remains gated on the post-S5 AMO preflight.
M4 YouTube Music Extras Flow¶
The MAIN-world layer owns one shared Web Audio graph per media element. It reads YouTube's per-track loudness value from the already-requested player response, applies a bounded gain, and chains the user's five EQ bands in series. The isolated content layer requests lyrics only after explicit opt-in; background calls the fixed LRCLIB endpoint without credentials or referrer, and content renders timed text safely.
sequenceDiagram
participant Main as MAIN world
participant Video as Page video
participant Content as Isolated content
participant Background as Background
participant LRCLIB as LRCLIB
Main->>Video: One MediaElementSource per element
Main->>Video: EQ filters then normalized GainNode
Main->>Content: Bounded track metadata
Content->>Background: Opt-in lyrics request
Background->>LRCLIB: GET /api/get, credentials omitted
LRCLIB-->>Content: Timed LRC via background
Content->>Video: Sync text-only lyric lines to currentTime Any graph, metadata, bridge, remote, parse, or DOM failure is a no-op. Scrobbling is out of scope because it conflicts with ghost mode.
M5 Audio Download Flow¶
An off-by-default in-player control initiates a fresh credentialless ANDROID_VR request in MAIN world. MAIN selects the preferred direct audio format and sends a nonce-authenticated JSON-string payload through isolated content. The background independently validates the Googlevideo URL and bounded canonical filename, then uses the downloads API directly. A credentialless Blob fallback is used only when the direct handoff fails.
sequenceDiagram
participant User
participant Content as Isolated content
participant Main as MAIN world
participant API as InnerTube
participant Background as Background
participant Downloads as Firefox downloads
User->>Content: Click Download audio
Content->>Main: Nonce + request ID (JSON detail)
Main->>API: Credentialless ANDROID_VR player POST
API-->>Main: Direct audio format + title
Main-->>Content: Validated URL + sanitized filename
Content->>Background: Fixed download operation
Background->>Background: Revalidate Googlevideo URL + filename
Background->>Downloads: Direct downloads.download Acquisition, bridge, validation, direct-download, and fallback failures return a bounded failure result and never alter playback.
Release and Distribution¶
One source tree feeds a single Firefox add-on identity, {580efa7d-66f9-474d-857a-8e2afc6b1181} (ADR-0006). Production is the AMO listed channel: the listed build omits update_url, and AMO is the sole update authority, delivering hands-off auto-update on Firefox desktop and Firefox for Android. A beta channel uses the same ID signed unlisted at a distinct pre-release version and is installed by hand for desktop and Android testing. Publishing to AMO is on demand (a manual run after testing), never automatic on a tag. AMO credentials (AMO_JWT_ISSUER / AMO_JWT_SECRET) exist only at signing time. The single ID is wired across wxt.config.ts, the bench ADDON_ID, and the workflows. .github/workflows/beta.yml signs the unlisted beta on a pre-release tag; .github/workflows/publish-amo.yml (manual workflow_dispatch only) signs the listed production version on demand. The self-hosted updates.json path from ADR-0004 is retired for production.
After all executable CI gates pass on a merge push to master, .github/workflows/ci.yml also publishes the current unsigned packaged XPI to a latest GitHub Release for archival and manual or temporary installation, then bumps the final numeric version segment in package.json with a [skip ci] commit. This archival artifact is not a signed distribution or update channel and does not replace AMO.
flowchart LR
Source[WXT source, single ID] --> Beta[Unlisted signed beta: pre-release version]
Source --> Prod[Listed build: no update_url]
Beta --> Install[Hand-installed desktop + Android testing]
Prod --> AMO[AMO listed signature, on-demand publish]
AMO --> Desktop[Firefox desktop auto-update]
AMO --> Android[Firefox Android auto-update] Build Outputs¶
.output/firefox-mv2/: shipping Firefox MV2 directory..output/firefox-mv3/: Firefox MV3 capability directory.dist/youtube-audio.xpi: stable packaged MV2 artifact consumed by the Selenium harness.dist/youtube-audio-<version>-signed.xpi: Mozilla-signed unlisted release artifact (created only with AMO credentials).