-
Notifications
You must be signed in to change notification settings - Fork 0
Add Twitch ad blocking, honest UI, and store-ready docs #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
1a18d12
establish passive live observation baseline
wpggLabs 4901bc6
feat: add client-side Twitch ad-blocking engine
wpggLabs 92fed5b
refactor: honest ad-blocking UI for popup and dashboard
wpggLabs 671fe5a
docs: rewrite README, add landing site and store submission guide
wpggLabs 04b5cca
refactor: centralize ad-block signatures in one patchable config
wpggLabs b921c3f
feat: mute player during ad swap to smooth ad boundaries
wpggLabs c068805
fix: resolve floating promises surfaced by no-floating-promises
wpggLabs dacf373
build: add ESLint with typed no-floating-promises, engines field, lin…
wpggLabs 1704276
ci: add GitHub Actions pipeline (lint, test, build, firefox lint, smoke)
wpggLabs 149396b
test: add opt-in live ad-block verification test
wpggLabs ee9f569
docs: add store promo graphic and listing-assets checklist
wpggLabs 015cc54
docs: add CI badge and signature-update guide to README
wpggLabs d06613c
test: stabilize diagnostics-disable assertion in smoke test
wpggLabs 6385635
docs: match house style for README and landing page, add OG/social me…
wpggLabs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| build-and-test: | ||
| name: Lint, test, build | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
| cache: npm | ||
|
|
||
| - name: Install dependencies | ||
| run: npm ci | ||
|
|
||
| - name: Type-check | ||
| run: npm run lint:types | ||
|
|
||
| - name: ESLint | ||
| run: npm run lint:style | ||
|
|
||
| - name: Unit + build-integrity tests | ||
| run: npm test | ||
|
|
||
| - name: Build both extensions | ||
| run: npm run extension:build | ||
|
|
||
| - name: Validate Firefox build | ||
| run: npm run test:firefox:lint | ||
|
|
||
| - name: Upload build artifacts | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: extensions | ||
| path: build/ | ||
| retention-days: 7 | ||
|
|
||
| smoke: | ||
| name: Chromium smoke test | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
| cache: npm | ||
|
|
||
| - name: Install dependencies | ||
| run: npm ci | ||
|
|
||
| - name: Install Playwright Chromium | ||
| run: npx playwright install --with-deps chromium | ||
|
|
||
| - name: Run smoke test | ||
| run: xvfb-run --auto-servernum npm run test:smoke:chromium |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,60 +1,127 @@ | ||
| <div align="center"> | ||
|
|
||
| <img src="TwitchShield/assets/icons/icon128.png" alt="TwitchShield" width="96" height="96" /> | ||
|
|
||
| # TwitchShield | ||
|
|
||
| TwitchShield is a local-first Chromium and Firefox browser extension that applies configurable request-blocking rules and player recovery behavior on Twitch pages. It has no backend, analytics, telemetry upload, or remote executable code. | ||
| #### Block Twitch ads — locally, privately, in Chrome & Firefox. | ||
|
|
||
| Twitch ad-block effectiveness has **not been verified with real Twitch playback**. The current automated coverage verifies unit simulations, package integrity, Firefox manifest validation, and Chromium popup/options loading. | ||
| [](https://github.com/wpggLabs/TwitchShield/actions/workflows/ci.yml) | ||
| [](https://wpgglabs.github.io/TwitchShield/) | ||
| [](TwitchShield/LICENSE) | ||
| [](https://www.typescriptlang.org/) | ||
| [](https://developer.chrome.com/docs/extensions/mv3/intro/) | ||
| [](#privacy) | ||
|
|
||
| ## Requirements | ||
| <img src="TwitchShield/assets/store/promo-tile.svg" alt="TwitchShield — block Twitch ads, privately" width="520" /> | ||
|
|
||
| - Node.js 20 or newer | ||
| - npm | ||
| <br /><br /> | ||
|
|
||
| ## Install and verify | ||
| <sub> | ||
| <a href="#overview">Overview</a> · | ||
| <a href="#features">Features</a> · | ||
| <a href="#install">Install</a> · | ||
| <a href="#how-it-works">How it works</a> · | ||
| <a href="docs/SUBMISSION.md">Store submission</a> · | ||
| <a href="#keeping-it-working">Maintaining</a> | ||
| </sub> | ||
|
|
||
| ```powershell | ||
| npm install | ||
| npm run lint | ||
| npm run test:unit | ||
| npm run test:integrity | ||
| ``` | ||
| </div> | ||
|
|
||
| --- | ||
|
|
||
| ## Overview | ||
|
|
||
| Twitch inserts ads directly into the video stream (server-side ad insertion, "SSAI"), | ||
| so the classic "block the ad request" approach does not work. TwitchShield runs inside | ||
| the Twitch player's media worker, inspects each HLS playlist, and transparently swaps | ||
| ad-stitched segments for an ad-free stream requested with a player type that Twitch does | ||
| not serve mid-rolls to. | ||
|
|
||
| Everything happens on your device. The extension makes no network calls except to | ||
| Twitch's own APIs, stores no personal data, and ships no analytics or remote code. | ||
|
|
||
| > **Honest expectations.** No Twitch ad blocker is perfect. Twitch changes its ad | ||
| > delivery frequently, so any blocker will occasionally let an ad through until it is | ||
| > updated. TwitchShield blocks the large majority of ads and is built to be easy to | ||
| > patch. See [Limitations](#limitations). | ||
|
|
||
| ## Features | ||
|
|
||
| ## Build | ||
| - **Stream-level ad removal** via HLS playlist interception — the technique that works against SSAI. | ||
| - **Per-channel whitelist** — support your favorite streamers by letting their ads play. | ||
| - **Blocked-ad counter** and an optional, opt-in on-device diagnostic log. | ||
| - **Mute-through-ad** option that silences the player while the clean stream loads. | ||
| - **Chrome + Firefox**, both on Manifest V3, from a single codebase. | ||
| - **100% local** — no backend, no telemetry, no account access. | ||
|
|
||
| ```powershell | ||
| ## Install | ||
|
|
||
| ### From source | ||
|
|
||
| ```bash | ||
| npm install | ||
| npm run extension:build | ||
| ``` | ||
|
|
||
| Generated unpacked extensions: | ||
| Then load the unpacked build: | ||
|
|
||
| - **Chrome / Edge** — open `chrome://extensions`, enable *Developer mode*, click **Load unpacked**, select `build/chromium`. | ||
| - **Firefox** — open `about:debugging#/runtime/this-firefox`, click **Load Temporary Add-on**, select `build/firefox/manifest.json`. | ||
|
|
||
| ### Packaged archives | ||
|
|
||
| - Chromium: `build/chromium/` | ||
| - Firefox: `build/firefox/` | ||
| ```bash | ||
| npm run pack:chromium # -> artifacts/twitchshield-chromium.zip | ||
| npm run pack:firefox # -> artifacts/twitchshield-firefox.zip | ||
| ``` | ||
|
|
||
| ## How it works | ||
|
|
||
| Load `build/chromium/` through `chrome://extensions` with Developer mode enabled. Load `build/firefox/manifest.json` through `about:debugging#/runtime/this-firefox` as a temporary add-on. | ||
| | Layer | Responsibility | | ||
| | --- | --- | | ||
| | `adblock.ts` (MAIN world) | Wraps `window.Worker` so the ad-block payload is injected into the Twitch player's HLS worker before it starts. | | ||
| | `adblock-worker.ts` | Runs inside the worker: detects stitched ad markers in each `.m3u8` playlist and substitutes an ad-free playlist. | | ||
| | `adblock-config.ts` | The single patchable ruleset — client id, ad markers, player types, token hash. | | ||
| | `content.ts` | Injects the engine at `document_start`, applies the whitelist/on-off state, records blocked ads as diagnostics. | | ||
| | `packages/core` | Shared, framework-free logic (settings, storage, privacy rules) used by both browser targets. | | ||
|
|
||
| ## Package | ||
| ## Development | ||
|
|
||
| ```powershell | ||
| npm run pack:chromium | ||
| npm run pack:firefox | ||
| ```bash | ||
| npm run lint # TypeScript strict type-check + ESLint | ||
| npm test # unit (privacy) + build-integrity tests | ||
| npm run extension:build # build Chromium + Firefox into build/ | ||
| npm run test:smoke:chromium # load the extension and open popup/options | ||
| npm run test:firefox:lint # validate the Firefox build with web-ext | ||
| ``` | ||
|
|
||
| Archives are written to `artifacts/`. Each archive contains `manifest.json` at its root. | ||
| CI runs the full suite on every push and pull request. | ||
|
|
||
| ## Keeping it working | ||
|
|
||
| ## Tests | ||
| When Twitch changes its ad delivery, the fix is almost always a one-file edit to | ||
| [`TwitchShield/packages/core/src/adblock-config.ts`](TwitchShield/packages/core/src/adblock-config.ts) — | ||
| the client id, ad markers, player types, and access-token hash all live there behind a | ||
| `signatureVersion`. No logic changes, no remote config fetch. Bump the version, rebuild, | ||
| and ship a release. To verify against a live channel: | ||
|
|
||
| ```powershell | ||
| npm test | ||
| npm run test:smoke:chromium | ||
| npm run test:firefox:lint | ||
| ```bash | ||
| RUN_LIVE=1 TWITCH_CHANNEL=<channel> npm run test:live:adblock | ||
| ``` | ||
|
|
||
| - `test:unit`: local player-state simulations only; no browser or Twitch traffic. | ||
| - `test:integrity`: builds both targets and verifies every manifest-referenced local asset. | ||
| - `test:smoke:chromium`: loads the unpacked Chromium extension and opens its popup and options pages. | ||
| - `test:firefox:lint`: validates the generated Firefox extension with Mozilla `web-ext`. | ||
| ## Limitations | ||
|
|
||
| - Effectiveness depends on Twitch's current ad delivery; expect occasional breakage until updated. | ||
| - Brief buffering can occur at ad boundaries while the clean stream is fetched. | ||
| - Live-ad effectiveness is not asserted in CI (non-deterministic); spot-check before releasing. | ||
|
|
||
| ## Privacy | ||
|
|
||
| No automated test currently verifies live Twitch playback or ad blocking. | ||
| - No cookies, tokens, headers, full URLs, account data, chat, or page snapshots are ever stored. | ||
| - Diagnostics are **off by default**, capped locally, and never leave your device. | ||
| - Host permissions are limited to `*://*.twitch.tv/*`. | ||
|
|
||
| ## Permissions | ||
| ## License | ||
|
|
||
| - `activeTab`: used only when the popup's whitelist action reads the active tab URL to identify the current Twitch channel. It does not provide persistent browsing access. | ||
| [MIT](TwitchShield/LICENSE) · © wpggLabs. Not affiliated with or endorsed by Twitch Interactive, Inc. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { AD_BLOCK_CONFIG, adBlockWorkerMain } from "../../packages/core/src/adblock-worker.js"; | ||
|
|
||
| /** | ||
| * MAIN-world entry point. | ||
| * | ||
| * Runs in the page's own JavaScript context (not the isolated content-script world) | ||
| * so it can replace the global `Worker` constructor before the Twitch player spins up | ||
| * its HLS worker. Every worker Twitch creates is wrapped with our fetch hook, which | ||
| * detects and removes server-side-inserted ads from the media playlist. | ||
| */ | ||
|
|
||
| declare const __BROWSER_TARGET__: string; | ||
|
|
||
| const FLAG = "__twitchShieldActive__"; | ||
| const globalScope = self as unknown as Record<string, unknown>; | ||
|
|
||
| function buildWorkerBlob(originalUrl: string): string { | ||
| const constantsLiteral = JSON.stringify(AD_BLOCK_CONFIG); | ||
| const hookSource = adBlockWorkerMain.toString(); | ||
| const payload = [ | ||
| `const __twitchShieldConstants = ${constantsLiteral};`, | ||
| `(${hookSource})(__twitchShieldConstants);`, | ||
| `importScripts(${JSON.stringify(originalUrl)});` | ||
| ].join("\n"); | ||
| return URL.createObjectURL(new Blob([payload], { type: "application/javascript" })); | ||
| } | ||
|
|
||
| function installWorkerHook(): void { | ||
| if (globalScope[FLAG]) return; | ||
| globalScope[FLAG] = true; | ||
|
|
||
| const NativeWorker = self.Worker; | ||
|
|
||
| const blockingEnabled = (): boolean => | ||
| document.documentElement.getAttribute("data-twitchshield-block") !== "off"; | ||
|
|
||
| class ShieldedWorker extends NativeWorker { | ||
| constructor(scriptURL: string | URL, options?: WorkerOptions) { | ||
| const url = scriptURL.toString(); | ||
| const isTwitchWorker = url.includes("twitch") || url.startsWith("blob:"); | ||
| if (isTwitchWorker && !url.includes("twitchShield") && blockingEnabled()) { | ||
| try { | ||
| super(buildWorkerBlob(url), options); | ||
| this.addEventListener("message", (event: MessageEvent) => { | ||
| const data = event.data as { twitchShield?: boolean; type?: string; channel?: string } | null; | ||
| if (data?.twitchShield) { | ||
| window.dispatchEvent(new CustomEvent("twitchshield:ad", { detail: data })); | ||
| } | ||
| }); | ||
| return; | ||
| } catch { | ||
| // Fall through to a native worker if blob construction is blocked. | ||
| } | ||
| } | ||
| super(scriptURL, options); | ||
| } | ||
| } | ||
|
|
||
| Object.defineProperty(self, "Worker", { | ||
| configurable: true, | ||
| writable: true, | ||
| value: ShieldedWorker | ||
| }); | ||
| } | ||
|
|
||
| function boot(): void { | ||
| if (typeof self.Worker !== "function") return; | ||
| installWorkerHook(); | ||
| void __BROWSER_TARGET__; | ||
| } | ||
|
|
||
| boot(); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The gate is evaluated only before wrapping a Worker. After a Twitch HLS worker has been constructed,
syncSettingsonly changesdata-twitchshield-blockin 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 👍 / 👎.