Paginate tag (and ASC) lists via GQL with integrity retry; mint follow integrity from a WebView because Dio /integrity tokens are bad_bot. Co-authored-by: Cursor <cursoragent@cursor.com>
3.6 KiB
Frosty fork — Twitch ad blocking (DESIGN)
Personal Forgejo fork of 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 +TwitchAdFreePlaybacklib/apis/twitch_gql_api.dart—playerType/platform,fetchPlaylistTextlib/screens/channel/video/native_video_store.dart— load + swaplib/screens/channel/video/video_store.dart— WebView injectlib/screens/settings/stores/settings_store.dart—blockTwitchAdsassets/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_ASCviaStreamSortButton
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) - Tag lists (asc + desc): GQL
streamswithfreeformTags+sort+ cursor. Page-2+ uses Client-Integrity (same mint/retry as follow viaUserStore.runWithGqlIntegrity). Helix + client tag filter only if integrity fails / no web session. - Follow / integrity: Dio
POST /integrityis almost alwaysis_bad_bot. Follow retries viaIntegrityWebViewDialog(captureClient-Integrity+X-Device-Idfrom Twitch's own WebView GQL traffic), thenUserStore.applyWebIntegrity. - Top / category most viewers (desc): Helix directory order (paginates)
- Top / category fewest (asc): GQL
VIEWER_COUNT_ASCwith the same integrity pagination; Helix fill + client re-sort as fallback
Build
Flutter 3.47.0 (pinned in pubspec.yaml). Local SDK: /home/yuna/.local/flutter-3.47.0.