Add Twitch ad blocking, honest UI, and store-ready docs - #3
Conversation
Intercept the HLS media playlist inside the Twitch player worker, detect server-side-inserted ad markers, and substitute an ad-free playlist obtained with a non-mid-roll player type. Injected into the page MAIN world via a web-accessible resource, gated by the per-channel whitelist and on/off state.
Reflect real settings state in the popup (fixes controls that ignored stored values), remove dead mode pills, replace alert() with inline status messages, show a live blocked-ad counter and current-channel whitelist toggle, and align all copy with ad blocking rather than passive observation.
Document the ad-blocking architecture and honest limitations, add a GitHub Pages landing page and Chrome Web Store / Firefox AMO submission guide, and remove superseded observation-phase reports.
Move the client id, ad markers, player types, and access-token hash into adblock-config.ts with a signature version, so Twitch-side changes are a single-file update. No remote config fetch, preserving the local-only guarantee.
Wire the muteDuringAds setting to briefly mute the player while the ad-free stream loads, with a user toggle in the dashboard.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 671fe5a5fb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (isTwitchWorker && !url.includes("twitchShield") && blockingEnabled()) { | ||
| try { | ||
| super(buildWorkerBlob(url), options); |
There was a problem hiding this comment.
Honor pause and whitelist in existing workers
The gate is evaluated only before wrapping a Worker. After a Twitch HLS worker has been constructed, syncSettings only changes data-twitchshield-block in the page; the injected fetch hook inside that already-running worker never reads that attribute or receives the setting. If the user clicks Pause or whitelists the current channel while watching, the existing worker keeps substituting playlists until reload, so ads are still removed even though the UI says they're allowed.
Useful? React with 👍 / 👎.
| type: "ad-stripped", | ||
| channel | ||
| }); | ||
| return new Response(text, { |
There was a problem hiding this comment.
Strip ads before reporting the fallback as blocked
When the clean-playlist lookup fails, this path returns text unchanged even though the request got here only after containsAd(text) was true. That means the player still receives the original stitched ad playlist, while the worker posts ad-stripped and the UI can count it as blocked. Either actually remove the stitched segments here or report/pass through as a failure instead of a successful strip.
Useful? React with 👍 / 👎.
| const diagnostics = await TwitchShieldStorage.getDiagnostics(); | ||
| const adsRemoved = diagnostics.filter((event) => event.eventType === "ad_ui_signal_detected").length; | ||
| blockedCount.textContent = String(adsRemoved); |
There was a problem hiding this comment.
Keep blocked counts independent of diagnostics
Diagnostics are disabled by default, and record() drops all ad events when diagnostics are off; deriving the popup counter only from stored diagnostics therefore leaves the Ads Blocked card at 0 for default users even when the worker removes ads. Store a privacy-safe aggregate counter separately from the opt-in diagnostic log, or update the UI copy so the count is only advertised when diagnostics are enabled.
Useful? React with 👍 / 👎.
| const result = await chrome.storage.local.get("settings"); | ||
| return { ...DEFAULT_SETTINGS, ...(result.settings || {}) }; |
There was a problem hiding this comment.
Restore Firefox async API compatibility
This path now selects any environment with chrome.storage.local, including the Firefox build, but then uses the Promise form of chrome.storage.local.get. Firefox exposes the chrome.* compatibility namespace as callback-based unless you use browser.*/a polyfill, so on Firefox this call resolves to undefined and result.settings throws before the popup, content script, or background can load settings. Keep the old callback/browser fallback or route Firefox through a promise wrapper.
Useful? React with 👍 / 👎.
| } | ||
| record("page_loaded", "Twitch page observation started."); | ||
| inspectPage(); | ||
| new MutationObserver(inspectPage).observe(document.documentElement, { childList: true, subtree: true }); |
There was a problem hiding this comment.
Recompute channel on Twitch SPA navigations
Twitch changes channels with in-page navigation, but after the initial derivePageContext() this observer only calls inspectPage(), so channel and the whitelist decision remain tied to the first URL until a reload or settings sync. If a user navigates from one channel to another, ads can stay blocked on a newly whitelisted channel (or stay disabled after leaving a whitelisted one), and diagnostics are attributed to the stale channel.
Useful? React with 👍 / 👎.
| script.src = chrome.runtime.getURL("adblock.js"); | ||
| script.type = "text/javascript"; | ||
| script.dataset.twitchshield = "engine"; | ||
| (document.head || document.documentElement).appendChild(script); |
There was a problem hiding this comment.
Install the worker hook before page scripts can run
The main-world hook is now loaded via a dynamically inserted external script, which does not block the parser or wait before Twitch's own early scripts can create the HLS Worker. When the player constructs its worker before adblock.js has fetched and executed, the native Worker is used and no playlist interception is installed for that page load. This needs a synchronous bootstrap or another guarantee that the hook is in place before Twitch can start the player worker.
Useful? React with 👍 / 👎.
Capture the baseline count after the disabled setting propagates rather than from a stale pre-disable snapshot, so late UI-change events do not flake CI.
Summary
Turns TwitchShield from a passive observer into a working, privacy-first Twitch ad blocker for Chrome and Firefox, with a professional engineering setup.
Ad blocking
adblock-config.tsbehind asignatureVersion, so updates are a one-file change.UI
alert()with inline status, and shows a live blocked-ad counter + per-channel whitelist toggle.Engineering
no-floating-promises(already caught + fixed two real bugs).RUN_LIVE=1) and extended build-integrity coverage.engines: node >=20.Docs & assets
Validation
npm run lint(type-check + ESLint) — cleannpm test(unit + build-integrity) — 7/7npm run test:smoke:chromium— passesnpm run test:firefox:lint— 0 errors, 0 warningsHonest limitations
No Twitch ad blocker is perfect: Twitch changes ad delivery frequently, so occasional ads may appear until
adblock-config.tsis updated. Live-ad effectiveness is not asserted in CI (non-deterministic) — spot-check withRUN_LIVE=1 npm run test:live:adblockbefore a release.