frosty/DESIGN.md
copilot 318dc84dc5
All checks were successful
Actions probe / probe (push) Successful in 0s
Release APK / Build & publish release (push) Successful in 7m37s
Fix tag infinite scroll after GQL ASC first page.
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>
2026-10-01 12:23:15 +02:00

60 lines
3.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`.