# Frosty fork — Twitch ad blocking (DESIGN) Personal Forgejo fork of [tommyxchow/frosty](https://github.com/tommyxchow/frosty) that adds **TwitchAdSolutions (vaft)**-style live ad blocking. Canonical living plan: session `active-plan.md` for conversation `37b527bf-e540-4753-be1f-39906fe2ba28`. ADR: **ADR-026**. ## Problem Upstream Frosty plays Twitch HLS (native) or `player.twitch.tv` (WebView). Twitch may stitch ad pods into the media playlist (`stitched` / `EXT-X-DATERANGE`). Upstream only **detects** native ad breaks (to avoid bogus latency recovery); it does **not** block ads. Turbo/sub `hide_ads` via linked web session is separate and not a client blocker. ## Approach (vaft-equivalent) Browser vaft hooks `Worker`/`fetch` and rewrites m3u8. Frosty’s native player cannot do that from Dart (`loadUrl` + headers only). | Layer | Behavior | | --- | --- | | Primary token | GQL `PlaybackAccessToken` with `playerType=popout` (vaft `ForceAccessTokenPlayerType`) when Settings `blockTwitchAds` is on | | Ad break | Native `adBreakStream` → probe backup types `embed` → `popout` → `autoplay`; accept first Usher multivariant whose sampled media playlist lacks `stitched`; `loadUrl` that URL | | Ad end | Restore primary popout Usher URL | | WebView | Best-effort inject of vendored `assets/js/vaft.user.js` (not true document-start on Android) | | UI | Settings toggle (default on; enabling forces native player); overlay “Blocking ads” | ## Key code - `lib/apis/twitch_ad_solutions.dart` — constants + `TwitchAdFreePlayback` - `lib/apis/twitch_gql_api.dart` — `playerType` / `platform`, `fetchPlaylistText` - `lib/screens/channel/video/native_video_store.dart` — load + swap - `lib/screens/channel/video/video_store.dart` — WebView inject - `lib/screens/settings/stores/settings_store.dart` — `blockTwitchAds` - `assets/js/vaft.user.js` — upstream TwitchAdSolutions script ## Non-goals (v1) - Upstream PR - Full Worker prototype cleaning - Third-party proxies - HLS segment stripping inside `better_native_video_player` (unless smoke tests demand it) ## Tags + viewer sort (v5.2.2+) Helix cannot filter by freeform tags or sort ascending. GQL covers both: - Search: `SearchFreeformTags` → tap → `TagStreams` (`streams` + `freeformTags`) - Sort: Top (asc), category (`game.streams`), tag — `VIEWER_COUNT` / `VIEWER_COUNT_ASC` via `StreamSortButton` Key code: `lib/apis/twitch_gql_api.dart`, `lib/screens/home/search/`, `lib/widgets/stream_sort_button.dart`, `ListStore` / `ListType.tag`. ## Stream languages (v5.2.4+) Settings → General → Stream languages (multi-select chips). Default `de`. Applied to Top, category, and tag lists. - Helix `language=` when one language; multi-language / tags via Helix over-fetch + client filter - Viewer sort is persisted (`streamViewerSort`) - **Most viewers (desc):** Helix directory order (paginates; tags via client `tags[]` match) - **Fewest viewers (asc):** GQL `VIEWER_COUNT_ASC` for the first ~30 (true fewest), then Helix fill + dedupe for infinite scroll (GQL page-2 needs Client-Integrity and always fails) - Tag directories never rely on GQL cursors for load-more ## Build Flutter **3.47.0** (pinned in `pubspec.yaml`). Local SDK: `/home/yuna/.local/flutter-3.47.0`.