frosty/DESIGN.md
copilot 3fb393166f Fix tag directory sorting and follow Client-Integrity.
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>
2026-10-07 07:32:16 +02:00

3.6 KiB
Raw Permalink Blame History

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 + 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)
  • Tag lists (asc + desc): GQL streams with freeformTags + sort + cursor. Page-2+ uses Client-Integrity (same mint/retry as follow via UserStore.runWithGqlIntegrity). Helix + client tag filter only if integrity fails / no web session.
  • Follow / integrity: Dio POST /integrity is almost always is_bad_bot. Follow retries via IntegrityWebViewDialog (capture Client-Integrity + X-Device-Id from Twitch's own WebView GQL traffic), then UserStore.applyWebIntegrity.
  • Top / category most viewers (desc): Helix directory order (paginates)
  • Top / category fewest (asc): GQL VIEWER_COUNT_ASC with 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.