GQL page-2 fails integrity, so fewest starts with GQL then continues via Helix tag filter with dedupe; empty Helix windows no longer end the list while a cursor remains. Co-authored-by: Cursor <cursoragent@cursor.com>
60 lines
3.2 KiB
Markdown
60 lines
3.2 KiB
Markdown
# 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`.
|