Flimmer

Entwicklerdoku · Version 0.5.28

Diese Doku beschreibt, wie Flimmer aufgebaut ist, warum es so gebaut ist und wie man daran weiterarbeitet. Die Anleitung für Nutzer steht in ANLEITUNG.md. Stand: Version 0.5.28 (24.10.2026).

1. Ziele und Grundentscheidungen

2. Schnellstart

Voraussetzungen: Deno 2, Rust (rustup), auf dem Mac die Xcode Command Line Tools, unter Linux WebKitGTK und GStreamer (siehe .github/workflows/build.yml).

bash
deno install
bash
. "$HOME/.cargo/env" && deno task tauri dev

tauri dev startet Vite (deno task dev, Port 1420) und die Debug-App. Der Rust-Bauordner ist src-tauri/target.noindex (über src-tauri/.cargo/config.toml), damit Spotlight das Bauergebnis nicht als zweite App anbietet.

Nur im Browser

Die Oberfläche läuft auch in Chromium. Weil der Browser YouTube nicht direkt abfragen darf (CORS), leitet ein kleiner Proxy die Anfragen weiter:

bash
deno task devproxy
bash
deno task dev

Dann http://localhost:1420 öffnen. Chromium verhält sich wie der Windows-Weg (shaka-player, DASH).

Umgebungsvariablen der App

VariableWirkung
FLIMMER_OPENÖffnet beim Start eine Seite, z. B. /watch?v=ID&t=SEK
FLIMMER_SETTINGSSetzt Einstellungen als JSON, z. B. {"player":{"engine":"shaka"}}. Gilt nur für die Sitzung: danach speichert die App keine Einstellungen mehr
FLIMMER_MEDIA_DEBUGDer Medien-Proxy protokolliert jede Anfrage
FLIMMER_CASTNamensteil eines Chromecast-Geräts: das beim Start geöffnete Video geht gleich dorthin (playback.autoCast, Player.svelte) – Fernseher-Tests ohne Klicken, z. B. FLIMMER_OPEN='/watch?v=ID' FLIMMER_CAST='Smart TV'

Beispiel ohne Klicken (Vite muss laufen):

bash
FLIMMER_OPEN='/watch?v=_2i-dhRl9wo&t=70' src-tauri/target.noindex/debug/flimmer

Die Zeilen [ui:…] (log() aus src/lib/log.ts) und [media] (Medien-Proxy) im Terminal zeigen, was passiert. Debug-App und installierte App haben getrennte Speicher (~/Library/WebKit/flimmer bzw. ~/Library/WebKit/io.github.mhd6271.flimmer).

Mac-App lokal bauen

bash
deno task tauri build

Ergebnis: src-tauri/target.noindex/release/bundle/macos/Flimmer.app (dazu ein .dmg). Windows- und Linux-Pakete baut GitHub Actions (*Actions → Bauen* von Hand starten oder ein Tag v… pushen); die Dateien hängen als Artefakte am Lauf.

Aufgaben

BefehlZweck
deno task devVite-Entwicklungsserver
deno task buildOberfläche nach build/
deno task checksvelte-check (Ziel: 0 Fehler, 0 Warnungen)
deno task testTests ohne Netz (tools/offline.test.ts)
deno task tauri dev / buildApp starten / bauen
deno task devproxyProxy für den Browser-Test
deno task probeDiagnose eines Videos (tools/probe-direct.ts)

3. Verzeichnisse

PfadInhalt
src/routes/Seiten: / (Start, Suche), /watch, /channel, /playlist, /subscriptions, /library, /settings, /about; +layout.svelte mit Kopfzeile, Seitenleiste, durchgehendem Player, Mini-Player, Update-Hinweis
src/lib/Logik: YouTube, Netz, Zustand, Speicher, Dienste
src/lib/player/Abspieltechniken (native, shaka, sabr, sabr-adapter, progressive, gemeinsame Schnittstelle engine)
src/lib/sabr/BotGuard-Token für SABR (Verständigung mit dem Frame)
src/botguard/frame.ts: Tauri-Startskript, das im versteckten youtube.com-Frame die Prüf-Token erzeugt; gebaut mit deno task botguard nach src-tauri/botguard/frame.js (committet)
src/lib/components/Svelte-Bausteine (Player, Karten, Kommentare, Live-Chat, Seitenleiste …)
src-tauri/src/Rust: lib.rs (Befehle, Start), media.rs (Medien-Proxy), lan.rs (WLAN-Server), cast.rs (Chromecast)
src-tauri/capabilities/Rechte des Fensters (Netzziele, Updater)
src-tauri/gen/android/Android-Projekt (u. a. MainActivity.kt)
src-tauri/gen/apple/iOS-Projekt (xcodegen, project.yml)
src-tauri/icons/Symbole, Quelle flimmer-ios.svg für iOS
tools/Diagnose- und Hilfsskripte (Deno), Release, Android/iOS-Bau, Tests
.github/workflows/build.ymlWindows- und Linux-Build (von Hand oder per Tag v*)

4. Architektur und Datenfluss

 Oberfläche (Svelte, WebView)
   │  YouTube.js ──proxyFetch──► Tauri-HTTP-Plugin (Rust) ──► youtube.com / youtubei.googleapis.com
   │  SponsorBlock / DeArrow ──fetch──► sponsor.ajay.app (CORS erlaubt)
   │
   │  <video> ◄── HLS direkt (Mac/iOS: WebKit spielt googlevideo selbst)
   │  <video> ◄── MSE ◄── shaka-player ◄── media://?u=… ──► media.rs ──► *.googlevideo.com
   │
   └─ Chromecast:  cast.svelte.ts ─invoke─► lan.rs (HTTP im WLAN) + cast.rs (CASTV2/TLS) ──► Fernseher
                   (nur SABR: cast-sabr.ts lädt die Stücke selbst und legt sie per cast_live_put in lan.rs ab)
  1. Netz: net.ts stellt proxyFetch bereit. In der App läuft jede YouTube-Anfrage über das Tauri-HTTP-Plugin (Rust, ohne CORS, eigener User-Agent), im Browser über tools/devproxy.ts. YouTube.js bekommt proxyFetch als fetch.
  2. Daten: yt.ts kapselt YouTube.js und liefert eigene, schlanke Typen (VideoItem, VideoDetails, StreamInfo …). Seiten sprechen nur mit yt.ts, nie direkt mit YouTube.js.
  3. Wiedergabe: Die Video-Seite lädt Details und Stream und füllt den globalen Zustand playback. Der Player im Layout wählt eine Abspieltechnik und hängt sie an das eine <video>.
  4. Medien: Auf Apple-WebKit lädt <video> die HLS-Listen und -Stücke direkt von googlevideo (WebKit braucht dafür kein CORS). Überall sonst holt shaka-player die Daten per fetch über media://; media.rs lädt sie in Rust und gibt sie mit CORS-Kopfzeilen zurück.
  5. Zustand liegt in Svelte-5-Runen-Modulen (*.svelte.ts), gespeichert über persist.svelte.ts im localStorage der WebView.

5. YouTube-Zugriff

src/lib/yt.ts (~870 Zeilen). YouTube.js wird einmal erzeugt (youtube()), mit proxyFetch und einem JavaScript-Ausführer für die Entschlüsselung der Adressen (sandbox.ts, Web Worker mit Zeitlimit; YouTube.js hat ab Version 14 keinen eingebauten Interpreter mehr).

Clients

ClientWofürWarum
VISIONOS (STREAM_CLIENT)Videostrom: HLS-Liste (hls_manifest_url), adaptive Formate, eine Datei mit Bild und TonLiefert HLS bis 4K ohne Prüf-Token, Adressen ohne n-Parameter
WEBBeschreibung, Kommentare, ähnliche Videos, Live-Chat (/next), Suche, KanäleVolle Metadaten
ANDROID_VRdash_manifest_url für laufende Live-Streams (getLiveDashUrl)YouTube.js kann für Live kein DASH erzeugen, VISIONOS hat für Live nur MPEG-TS-HLS. Nicht als Rückfallweg für normale Videos: ohne Prüf-Token liefert er nur etwa die erste Minute (gemessen 22.10.2026: 1080p-Videospur bis ~15 MB, Tonspur bis ~0,4 MB, danach HTTP 403; VISIONOS dieselbe Datei durchgehend) – eingebaut in dc82000, wieder entfernt in 1d51b83
WEB + SABR (getSabrStream)Ausweichweg, wenn VISIONOS scheitertSo holt die YouTube-Webseite Videos; braucht ein BotGuard-Token

Warum VISIONOS? Gemessen am 13.10.2026 (deno task probe): Er liefert eine HLS-Adresse und Format-Adressen ohne Prüf-Token. WEB liefert nur noch SABR; IOS, MWEB und ANDROID_VR brechen ohne Token nach etwa 1 MB ab; TV ist gesperrt.

Live: VISIONOS liefert für Live nur HLS bis 720p aus MPEG-TS-Stücken mit eigener Tonspur (AAC mit ID3-Kopf). Der Mac-Systemplayer spielt das; shaka kann es nicht umwandeln (Fehler 3018). Für shaka holt Flimmer deshalb YouTubes eigenes DASH-Manifest über ANDROID_VR (fMP4-Stücke bis 1080p, ohne Prüf-Token ladbar; gemessen 13.10.: 15 Stücke, 12 MB, alle HTTP 200). SABR geht für Live nicht, und Live wird nicht im Verlauf fortgesetzt.

Der Weg zum Stream

stream.ts → loadStream(id, forceSabr):

getStream gibt StreamInfo zurück:

FeldInhalt
hlsAdresse der HLS-Hauptliste
dashFunktion, die ein DASH-Manifest aus den adaptiven Formaten baut (nicht bei Live)
liveDashFunktion, die die Live-DASH-Adresse liefert (withMpdVersion hängt /mpd_version/7 an)
progressiveEntschlüsselte Adresse der Datei mit Bild und Ton (meist 360p)
sabrDaten für SABR (nur aus getSabrStream)
audioTracks, captions, durationSeconds, isLive, …Begleitdaten

PlaybackError trägt YouTubes Begründung („Melde dich an …“, „Video privat“).

KI-Kennzeichnung

YouTube legt sie in /next als Abschnitt howThisWasMadeSectionViewModel („Wie wurde dieser Inhalt erstellt?“) in der Beschreibung ab; YouTube.js parst ihn als HowThisWasMadeSectionView. Derselbe Abschnitt steht auch bei automatischer Synchronisation. Unterschieden wird sprachunabhängig am verlinkten Hilfeartikel: answer/15447836 = mit KI erstellt/verändert, answer/15569972 = Synchronisation (aiLabelFrom → VideoDetails.aiLabel). Gefunden mit tools/probe-ai-label.ts; dort zeigte sich auch, wie lückenhaft die Selbstauskunft ist (offensichtliche „AI-Generated“-Trailer ohne Kennzeichnung).

Anfragen sparen (18.10.2026): getDetails holt nur noch /next und teilt sich die /player-Antwort (VISIONOS) mit getStream (visionosPlayer, 60 s gemerkt) – vorher stellte YouTube.js' getInfo eine zweite /player-Anfrage (WEB), ein Drittel aller Anfragen je Video. Während einer Sperre gibt es dann keine zusätzliche Anfrage; es fehlen höchstens Dauer und Originaltitel. Ebenso: Originaltitel nur für sichtbare Karten (siehe Externe Dienste), Live-Chat pausiert bei verborgenem Fenster, Abo-Feed stündlich und nicht während einer Sperre.

Bestandsaufnahme der Anfragen (24.10.2026, nach einer Sperre an einem Testtag): Je geöffnetem Video gehen an YouTube /player (VISIONOS, geteilt), /next (Details) und – nur auf Tippen, seit 0.5.29 (ui.commentsAuto lädt wie früher von selbst; die Anzahl kommt aus /next) – ein weiteres /next für die Kommentare; dazu oEmbed für jede neue Karte in der Liste „ähnliche Videos“ (≈ 20, gemerkt) und beim Weiterspielen dasselbe für das nächste Video. Scheitert VISIONOS: bis zu zwei weitere /player (WEB, MWEB) plus BotGuard (zwei Aufrufe an googleapis) und die laufenden SABR-Anfragen an googlevideo. Der Abo-Feed stellt je Kanal eine /browse-Anfrage, der Fernseher beim Verweilen auf einer Karte eine /player-Anfrage (Vorladen). Darauf gedrosselt: Vorladen erst nach 3 s Ruhe und höchstens eins je 15 s (stream.ts); onVisible (actions.ts) feuert erst, wenn ein Element 700 ms in Sicht bleibt – schnelles Scrollen löst keine oEmbed-, DeArrow- oder Kommentar-Anfragen aus; oEmbed eine Anfrage nach der anderen mit 400 ms Abstand; Abo-Feed ein Kanal nach dem anderen mit 750 ms Pause. net.ts zählt die Anfragen je Art, das Layout schreibt alle 10 Minuten „YouTube-Anfragen: n (player …, next …, browse …)“ ins Protokoll. Beim Testen gilt: keine curl-Abrufe von /watch-Seiten und keine Probe-Skripte mit eigenem Innertube.create gegen YouTube vom selben Anschluss – jede davon zählt wie ein neuer Client; am 24.10. kam die Sperre nach Emulator-Läufen, Probe-Skripten und etwa einem Dutzend Teststarts binnen zwei Stunden (nur IPv6 betroffen, IPv4 blieb frei).

getDetails merkt Ergebnisse 5 Minuten (höchstens 50): Das Weiterspielen prüft das nächste Video vorab (pickAutoplay in playback.svelte.ts, Einstellung player.skipAi), die Video-Seite braucht dieselben Daten gleich danach.

Weitere Funktionen

search (mit Filter Videos/Kanäle/Playlists, Fortsetzung über Page.next), getDetails (WEB, inkl. liveChat, ähnliche Videos, Originaltitel), getChannel/getChannelVideos/getChannelPlaylists, resolveChannel (@Name, /c/…, /user/…), getLatestVideos (Abo-Feed über den Videos-Tab mit params: EgZ2aWRlb3PyBgQKAjoA, weil YouTubes RSS-Feeds 404 liefern), getPlaylist, getComments, parseVideoId/parsePlaylistId/parseStartTime (Links), parseRelativeDate.

6. Wiedergabe: Wege und Abspieltechniken

Jede Technik implementiert Engine aus player/engine.ts:

ts
interface Engine {
  kind: 'hls' | 'shaka' | 'progressive' | 'sabr';
  onfail?: (reason: string) => void;
  attach(video, stream, quality, audio?): Promise<void>;
  setAudio(track): Promise<void>;
  qualities(): QualityOption[];
  setQuality(choice): Promise<void>;
  destroy(): Promise<void> | void;
}
TechnikDateiWoWie
NativeHlsEnginenative.tsMac, iOS (Apple-WebKit)video.src = HLS-Liste. Für Qualitätskappung und Tonspur baut sie eine eigene Hauptliste (hls.ts: capMaster, buildMaster) als data:-Adresse. Bild-in-Bild und AirPlay kommen vom System
ShakaEngineshaka.tsWindows, Linux, Android, Chromiumshaka-player 4.16 über MSE, DASH zuerst (HLS nur ohne DASH), DASH-Manifest als Blob-Adresse (Live: echte Adresse, shaka lädt sie alle 5 s neu). Alle Anfragen über media:// (proxyRequests)
SabrEnginesabr.ts + sabr-adapter.tsAusweichweg überall mit MSEshaka-player mit eigenem Netz-Adapter (ShakaSabrAdapter aus googlevideo), POST-Anfragen über media://
ProgressiveEngineprogressive.tsletzte Notlösungvideo.src = Datei mit Bild und Ton

Auswahl im Player

Player.svelte baut je Stream eine Kandidatenliste und probiert sie der Reihe nach (start). Meldet eine Technik onfail, kommt die nächste:

  1. SABR-Stream → nur SabrEngine.
  2. player.engine = native → NativeHlsEngine; shaka → ShakaEngine.
  3. auto auf Apple-WebKit → NativeHlsEngine, dann ShakaEngine.
  4. auto sonst → ShakaEngine.
  5. Danach ProgressiveEngine, zuletzt NativeHlsEngine, falls noch nicht dabei.

Scheitert eine Technik erst beim Abspielen (Fehler am <video>), kommen zuerst die übrigen Kandidaten dran (runCandidates, fallbacks), seit 0.5.16; vorher ging es dann sofort zu SABR. Der Systemplayer versucht vorher selbst die Original-Liste statt der aufbereiteten (recoverFromError). shaka gibt nach drei HTTP-403-Antworten auf (shaka meldet sie nur als behebbar und versuchte es endlos). Scheitert am Ende auch SABR, zeigt der Player „YouTube gibt dieses Video gerade nicht an Flimmer heraus“ mit „Auf YouTube öffnen“ (BLOCKED). Anlass (18.10.2026): ein heute-show-Video, bei dem YouTube über HLS keinen Ton lieferte (alle Tonspuren 404, nur VP9-Bild), DASH mit 403 und SABR mit leeren Antworten – vermutlich nur mit gültigem Prüf-Token.

Scheitern alle Kandidaten des VISIONOS-Wegs, lädt die Video-Seite den SABR-Stream (onfailure → loadStream(id, true)); im Mini-Player ohne Video-Seite übernimmt das fallbackInMini.

Fehlerbehandlung

7. Der durchgehende Player

Video und Bild-in-Bild sollen einen Seitenwechsel überstehen. Darum liegt der Player im Layout (+layout.svelte), nicht auf der Video-Seite:

8. Tonspur, Qualität, Untertitel, SponsorBlock

Tonspur (audio.ts, hls.ts, native.ts)

Qualität

Untertitel

SponsorBlock (sponsorblock.ts)

9. Rust: Medien-Proxy, WLAN-Server, Chromecast

lib.rs

Befehle für die Oberfläche: log (schreibt [ui:level] ins Terminal), startup_options (FLIMMER_OPEN, FLIMMER_SETTINGS, FLIMMER_CAST), open_text_file/save_text_file (Abo-Import und -Export über den Dateidialog), cast_devices/cast_start/cast_status/cast_control, cast_serve/cast_unserve, das Stücklager cast_live_*, set_webview_ua. Registriert das Schema media und die Plugins (http, opener, Updater und process nur auf dem Desktop). Das Hauptfenster legt setup selbst an (WebviewWindowBuilder::from_config, in tauri.conf.json create: false), weil nur so ein Startskript für alle Frames mitgegeben werden kann (initialization_script_for_all_frames) – siehe den nächsten Abschnitt.

src/botguard/frame.ts – Prüf-Token im youtube.com-Frame

Googles BotGuard prüft die Adresse der Seite, auf der es läuft: Mit https://www.youtube.com/ gibt GenerateIT ein Integritäts-Token zurück, mit der App-Adresse (tauri://localhost, http://tauri.localhost) nur das unbrauchbare Ersatz-Token und keine Signal-Funktion (in Deno + jsdom nachgestellt, 24.10.2026 – ein verpacktes window/document/location im Hauptfenster half nicht, die VM kommt an das echte Fenster). Deshalb legt src/lib/sabr/botguard.ts beim ersten SABR-Video einen versteckten <iframe> auf https://www.youtube.com/robots.txt?flimmer-botguard=1 an (winzig, Text, ohne Skripte, ohne X-Frame-Options, ohne CSP). Tauris Startskript frame.ts läuft in jedem Frame, tut aber nur dort etwas: Create → Interpreter laden → BotGuardClient.snapshot → GenerateIT → WebPoMinter, Anfragen an jnn-pa.googleapis.com direkt (CORS erlaubt youtube.com), Token-Wünsche und Antworten über postMessage (`{ flimmer: 'flimmer-botguard', type: 'mint' | 'token'

'error''log''ready' }`, Herkunft geprüft). Fehlschläge werden nach 60 s erneut versucht. Das Skript wird mit

deno bundle gebaut (deno task botguard, Teil von deno task build) und per include_str! eingebunden – nach Änderungen an frame.ts neu bauen und die frame.js mitcommitten. Ohne Tauri (Vite im Browser) gibt es kein Startskript, dann bleibt es beim vorläufigen Token. Mac-App: „BotGuard: bereit nach ~250 ms (1 Signale)“.

Offen (24.10.2026): Trotz echtem Token nahm der Datenstrom-Server die SABR-Anfragen nach wenigen Läufen nicht mehr an (Schutzstatus 2, dann Antworten ohne Material; der erste Lauf spielte über 45 s). Versuch mit dem User-Agent des WebViews für die SABR-Abrufe (set_webview_ua, mediaUrl(url, { webviewUa: true }), withUserAgent fürs HTTP-Plugin) brachte mit dem nackten WKWebView-UA nur noch leere 200er – vorerst aus (webviewUa: false). sabr-adapter.ts protokolliert bei leeren Antworten jetzt Status, Schutzstatus, Fehler, Umleitung, Kontext-Update (describeEmpty). Nicht in kurzer Folge testen: Jeder abgebrochene SABR-Lauf scheint den Anschluss weiter einzuschränken.

media.rs – Medien-Proxy

lan.rs – Medienserver im WLAN

cast.rs – Chromecast

10. Lokale Daten

Alles liegt im localStorage der WebView (persist.svelte.ts: loadJson, saveJson, persist speichert bei Änderung, entprellt).

SchlüsselModulInhalt
flimmer.settings.v1settings.svelte.tsEinstellungen (Abschnitte sponsorblock, dearrow, player, subtitles, chat, updates, ui)
flimmer.history.v1history.svelte.tsVerlauf (bis 200) mit Stelle und Dauer
flimmer.subscriptions.v1subscriptions.svelte.tsAbos
flimmer.feed.v1feed.svelte.tsZwischenspeicher des Abo-Feeds je Kanal (Auffrischen frühestens nach 60 min, ein Kanal nach dem anderen mit 750 ms Pause, nicht während einer Bot-Sperre)
flimmer.library.v1library.svelte.tsSpäter ansehen, eigene und gemerkte Playlists
flimmer.original-titles.v1titles.svelte.tsGemerkte Originaltitel
flimmer.sayings.v1sayings.tsAngebrochene Runde der Startseiten-Sprüche (n, restliche Reihenfolge, zuletzt gezeigter) – ohne sie mischte jeder Start neu, und nach gut einem Dutzend Starts wiederholte sich erfahrungsgemäß einer
flimmer.original-titles-missing.v1titles.svelte.tsVideo-ID → Zeitpunkt, zu dem oEmbed keinen Titel hatte; 7 Tage nicht erneut fragen (bis 0.5.18 eine Liste ohne Verfall – ein 403 vom Proxy wäre für immer geblieben)
flimmer.bot-blocks.v1blocks.svelte.tsBot-Sperren: laufende (Beginn, zuletzt gesehen) und die letzten 20 mit Ende
flimmer.hidden-channels.v1blocks.ts, blocks.svelte.tsisBotBlock, typicalMinutes (Median der letzten zehn Sperren, sonst 30 min), blocks, noteBlocked, noteOk – passiv aus loadStream; Anzeige unter dem Video auf dem Ausweichweg (seit wann, meist wie lange, Leiste). Gemessen werden kann mit tools/probe-block.ts (höchstens alle 30 min aufrufen)
activity.svelte.tsactivity, whileSearching (zählt laufende Suchen – der Fernseher oben links, Logo busy, hüpft, wackelt mit den Antennen und verstreut Konfetti, solange gesucht wird)
hidden.svelte.tsAusgeblendete Kanäle (ID und Name; ähnliche Videos kennen manchmal nur den Namen)

Beim Laden werden gespeicherte Einstellungen abschnittsweise mit den Standardwerten zusammengeführt. Neue Felder bekommen so automatisch ihren Standard. Speicherorte auf dem Mac: ~/Library/WebKit/io.github.mhd6271.flimmer/… /localstorage.sqlite3 (Werte als UTF-16).

11. Externe Dienste

DienstModulAnfrage
YouTube InnerTubeyt.tsüber das HTTP-Plugin, ohne Konto
googlevideoPlayer, media.rsVideodaten
SponsorBlocksponsorblock.tsHash-Präfix der ID; countSkip mit UUID
DeArrowdearrow.svelte.tsapi/branding/<Hash-Präfix>; Vorschaubilder über dearrow-thumb.ajay.app mit Video-ID (Standard aus)
Originaltiteltitles.svelte.tsYouTube oEmbed je Video – nur für sichtbare Karten (onVisible), nie während einer Bot-Sperre oder bei verborgener App, eine Anfrage nach der anderen mit 400 ms Pause (Karte muss 700 ms sichtbar bleiben), nach Fehlern frühestens nach 15 min erneut. Vorher fragte jede Karte beim Erzeugen (≈ 20 Anfragen je Video-Seite, beim Weiterspielen alle paar Minuten) – vermutlich ein Grund für schnell wiederkehrende Sperren
BotGuardsabr/botguard.ts, src/botguard/frame.tsnur im SABR-Weg, Skript von Google über jnn-pa.googleapis.com, läuft im youtube.com-Frame
Updatesupdates.svelte.tslatest.json aus mhd6271/flimmer-releases

12. Live-Chat

livechat.ts wandelt YouTube.js-Chat-Aktionen in eigene Typen (ChatMessage, ChatBanner, ChatChange) und wendet Änderungen an (applyChange, höchstens CHAT_LIMIT Nachrichten). LiveChat.svelte startet getLiveChat(), zeigt an und verbindet neu (nach 5, 15, 45 s).

YouTube.js' LiveChat fragt live_chat/get_live_chat in YouTubes Takt ab, verteilt die Nachrichten gleichmäßig und holt alle 5 s die Zuschauerzahl (updated_metadata), wie die Webseite. Chat-Wiedergaben beendeter Streams zeigt Flimmer nicht.

13. Updates und Veröffentlichen

Updater (Desktop)

Updater (Android, seit 0.5.18)

Schlüssel

DateiZweck
~/.tauri/flimmer-updater.key + .passwordSignatur der Update-Pakete; auch als Geheimnis TAURI_SIGNING_PRIVATE_KEY(_PASSWORD) im privaten Repo
~/.tauri/flimmer-android.jks + .passwordSignatur der APK, eingebunden über src-tauri/gen/android/keystore.properties (in .gitignore)

Geht der Update-Schlüssel verloren, nehmen installierte Apps keine Updates mehr an. Ein neuer Android-Schlüssel zwingt zum Deinstallieren. Beide nie committen oder ausgeben, Sicherungskopien aufbewahren.

Veröffentlichen

  1. Version in package.json, src-tauri/tauri.conf.json, src-tauri/Cargo.toml (about.ts liest sie aus package.json) erhöhen, committen.
  2. Notizen in eine Datei schreiben, dann:
bash
tools/release.sh NOTIZEN.md

Das Skript:

  1. prüft, dass die Arbeitskopie sauber ist und es das Release noch nicht gibt;
  2. setzt das Tag vX.Y.Z im privaten Repo und pusht es → GitHub Actions baut Windows (NSIS, MSI) und Linux (AppImage, deb, rpm) signiert;
  3. baut die Mac-App lokal (--bundles app) mit Update-Signatur, packt das Zip;
  4. baut die Android-APK lokal (nur wenn keystore.properties existiert);
  5. wartet auf den CI-Build (fragt alle 30 s, höchstens 40 min, übersteht Netzaussetzer) und lädt die Dateien;
  6. schreibt latest.json (darwin-aarch64, windows-x86_64, linux-x86_64 mit Signaturen);
  7. legt das Release in mhd6271/flimmer-releases als Entwurf an, lädt jede Datei einzeln mit Wiederholungen hoch (große Dateien brachen mit HTTP 408 ab) und veröffentlicht erst am Ende. So sieht keine App eine latest.json, deren Dateien noch fehlen.

Ein Release hat 15 Dateien. Die öffentlichen Releases liegen getrennt, weil die Pipeline des privaten Repos ohne zusätzlichen Zugangsschlüssel nicht ins öffentliche schreiben darf; das Skript nutzt die gh-Anmeldung.

14. Build-Profil (gemessen)

src-tauri/Cargo.toml:

toml
[profile.release]
codegen-units = 1
lto = true
opt-level = 3
panic = "abort"
strip = true

Am 18.10.2026 lokal gemessen (MacBook Air M5, nur der Rust-Teil der Release-Fassung):

ProfilVoller BauNach einer ÄnderungProgrammdatei
Aktuell: lto = true, codegen-units = 197 s61 s8,8 MB
Locker: lto = "thin", codegen-units = 1658 s24 s11,7 MB (+32 %)

Entscheidung: so lassen. Ein Release dauert insgesamt 20–25 Minuten; die meiste Zeit gehört dem CI-Build (Windows, Linux mit Abhängigkeiten, Bündeln) und dem Hochladen. Das lockere Profil spart dort geschätzt nur 1–2 Minuten, macht aber jede Programmdatei ein Drittel größer, also auch jeden Download und jedes Update. Für schnelles Arbeiten gibt es tauri dev (Debug-Profil). Lohnt sich später ein Wechsel, dann nur gemessen und über ein eigenes Profil, nicht durch Lockern von release.

15. Android

Einmalig einrichten: Java 17 (brew install openjdk@17), Android-Befehlszeilenwerkzeuge (brew install android-commandlinetools), per sdkmanager Platform-Tools, Android 35, Build-Tools 35, NDK 27.2, Rust-Ziele (rustup target add aarch64-linux-android …). Die Pfade setzt tools/android-env.sh.

bash
source tools/android-env.sh && deno task tauri android build --apk --target aarch64

Ergebnis unter src-tauri/gen/android/app/build/outputs/apk/universal/release/. tools/release.sh baut die APK mit.

Google Play (seit 22.10.2026 vorbereitet)

Alles für einen geschlossenen Test liegt in play/ (play/LIESMICH.md: Schritte in der Play Console, Antworten für Datensicherheit, Inhaltsbewertung, Vordergrunddienst, Entscheidung zur App-Signatur, Richtlinien-Risiko).

16. iOS

Noch nicht verteilt; wartet auf das Apple-Entwicklerkonto (Team-ID).

Einmalig einrichten: Xcode aus dem App Store mit iOS-Plattform, sudo xcode-select -s /Applications/Xcode.app, Rust-Ziele aarch64-apple-ios und aarch64-apple-ios-sim, brew install xcodegen cocoapods.

Bauen für den Simulator:

bash
tools/ios-build.sh

Das Skript setzt CARGO_TARGET_DIR (Xcodes Bauschritt übergeht .cargo/config.toml, Tauri suchte die Bibliothek sonst vergeblich in target.noindex) und legt .metadata_never_index an, damit Spotlight die Simulator-Apps nicht als weitere „Flimmer“ anbietet. tools/ios-build.sh --debug baut die Debug-Fassung. Starten: xcrun simctl install booted …/release-iphonesimulator/Flimmer.app, dann xcrun simctl launch booted io.github.mhd6271.flimmer. Der erste Start im frischen Simulator bleibt einige Sekunden weiß.

17. Testen und Diagnose

Diagnose-Skripte (tools/)

SkriptPrüft
probe.ts, probe-direct.tsClients und Formate eines Videos
probe-hls.ts, probe-hls-format.ts, print-hls.tsHLS-Listen, Varianten, Tonspuren
probe-audio.tsTonspuren und ihre Kennungen
probe-captions.tsUntertitel
probe-cors.tsWelche Server CORS erlauben
probe-titles.tsOriginaltitel (oEmbed, Player-Antwort)
probe-ai-label.tsKI-Kennzeichnung in /next (auch --search "Begriff")
probe-channel.ts, probe-feed.ts, probe-lockup.ts, probe-nodes.tsKanal-, Feed- und Listen-Antworten
probe-live*.ts, probe-livechat*.tsLive-Manifeste, -Stücke, Chat und Ablauf der Kennung
print-format-url.tsEntschlüsselte Format-Adresse
find-live.ts, find-sponsor.tsTestvideos finden

Wenn YouTube etwas ändert

  1. Protokoll der App lesen (Einstellungen → Protokoll, oder Terminal).
  2. Mit deno task probe (Format-Adressen und HLS je Client) und tools/probe.ts (SABR) prüfen, was gerade geht. Was die App selbst anfragt: window.__flimmerNet (Devtools) – die letzten 500 YouTube-Anfragen aus proxyFetch, nur Zeit, Methode, Adresse ohne Parameter. Gezählt 18.10. am Google TV: Suche = 1× search + oembed je sichtbarer Karte; Video = 1× /next + 1× /player (ohne SABR-Umweg). Live: probe-live.ts ID, probe-live-detail.ts ID, probe-live-segments.ts ID, probe-livechat.ts ID [s] [all|switch], probe-livechat-expiry.ts ID.
  3. YouTube.js aktualisieren (deno outdated, deno add npm:youtubei.js@latest), dann deno task check und deno task test.
  4. In der App unter Einstellungen → Wiedergabe → Für Fortgeschrittene Abspieltechnik und Videostrom erzwingen.
  5. Parser-Fehler zeigen sich meist in yt.ts (videosFrom, Lockup-Knoten); probe-lockup.ts/probe-nodes.ts zeigen die neuen Strukturen.

18. Stolperfallen

19. Sicherheit

20. Symbol-Referenz

src/lib

DateiWichtige Symbole
yt.tsyoutube, search, getDetails, getStream, getSabrStream, withMpdVersion, getChannel, getChannelVideos, getChannelPlaylists, getLatestVideos, resolveChannel, getPlaylist, getComments, videosFrom, parseVideoId, parsePlaylistId, parseStartTime, parseRelativeDate, PlaybackError; Typen VideoItem, ChannelItem, PlaylistItem, Page, VideoDetails, StreamInfo, SabrInfo, CaptionTrack, CommentItem
stream.tsloadStream, prefetchStream
net.tsproxyFetch, inTauri, isMobile, DEV_PROXY_URL
media.tsmediaUrl, isGoogleVideo
hls.tsfetchMaster, parseMaster, qualityLevels, capMaster, buildMaster, originalAudioId, toDataUrl, cleanAutoVtt
audio.tschooseAudio, audioLabel, AUDIO_CHOICE_LABELS, AudioTrackInfo
playback.svelte.tsplayback (mit handover), playerSlot, playNextInMini, fallbackInMini, pickAutoplay, peekNext, PlaybackProps, PlayerApi
sponsorblock.tsCATEGORIES, MODE_LABELS, fetchSegments, countSkip, Segment
dearrow.svelte.tsrequestBranding, brandedTitle, brandedThumbnail
hidden.svelte.tshidden, isHidden, hideChannel, unhideChannel (Filter in VideoCard, ChannelCard, pickAutoplay)
titles.svelte.tsrequestOriginalTitle, originalTitle, rememberOriginalTitle
settings.svelte.tssettings, applySettingsOverride, resetSettings, Settings
history.svelte.tshistory, rememberVideo, updatePosition, resumePosition, findEntry, removeEntry, clearHistory
subscriptions.svelte.tssubscriptions, subscribe, unsubscribe, isSubscribed, refreshSubscription, importSubscriptions, sortedSubscriptions
subscriptions-io.tsparseSubscriptionFile (NewPipe, FreeTube, Takeout-CSV, OPML, YouTube-JSON), exportNewPipe
feed.svelte.tsfeed, refreshFeed, feedVideos, lastRefresh
library.svelte.tsWATCH_LATER, library, createPlaylist, renamePlaylist, deletePlaylist, addToPlaylist, removeFromPlaylist, moveInPlaylist, isInPlaylist, savePlaylist, unsavePlaylist, isSaved, entriesAsVideos
queue.tsloadQueue, Queue
livechat.tsconvertAction, convertItem, applyChange, partsFrom, chatTime, CHAT_LIMIT
cast.svelte.tscast, castAvailable, findDevices, castDash, serveForLan, startCast, control, stopCast
cast-sabr.tsserveSabrForLan → LiveSession (startStream, gate, pause/resume, pump, put, waitForStart, stop): SABR-Stücke fürs Stücklager in lan.rs
updates.svelte.tsupdates, startUpdateChecks, checkForUpdate, installUpdate, restartNow
persist.svelte.tsloadJson, saveJson, persist
sandbox.tsevalInWorker
sha256.tssha
log.tslog, logLines, installErrorLogging
files.tsopenTextFile, saveTextFile
sayings.tsSAYINGS, nextSaying (Sprüche der Startseite, knapp 180: sicher zugeordnete Klassiker aus Antike und Weisheitslehren, Sprichwörter – tatarische aus der Isänbät-Sammlung, wörtlich übertragen –, 18 Stellen aus dem Hagakure in eigener Übertragung, Selbstfürsorge und eigene Witze; bewusst ohne Online-Dienst, zufällige Reihenfolge ohne Wiederholung – die angebrochene Runde liegt in flimmer.sayings.v1, damit ein Neustart nicht neu mischt; ändert sich die Anzahl, wird neu gemischt, und eine neue Runde beginnt nie mit dem zuletzt gezeigten)
format.tsformatCount, formatClock, parseDescription (Zeitmarken, Links), displayTitle (Kurztitel für AirPlay/Benachrichtigung)
actions.tsonVisible (Nachladen beim Scrollen), dismiss (Menü schließen bei Klick außerhalb)
external.tsopenExternal
about.tsAPP_VERSION, DEVELOPER, LIBRARIES, SERVICES

src/lib/player

DateiSymbole
engine.tsEngine (mit freezeQuality), AttachOptions (audioOnly), QualityOption, isAppleWebKit, QUALITY_HEIGHTS, qualityLabel, restorePosition
native.tsNativeHlsEngine (audioId, enforceAudio, AirPlay-Protokoll)
shaka.tsShakaEngine, loadShaka, msePossible, manifestUrl, proxyRequests, selectAudioTrack, audioPreferences, selectMaxHeight, applyQuality, MOBILE_AUTO_MAX
crossfade.tsSidePlayer (unsichtbarer Ton-Player fürs Überblenden)
sabr.tsSabrEngine
sabr-adapter.tsShakaSabrAdapter
progressive.tsProgressiveEngine
../sabr/botguard.tsmintToken, coldStartToken (Frame anlegen, postMessage)
src/botguard/frame.tsStartskript im youtube.com-Frame: createMinter, Nachrichten mint/token/error/log/ready

Komponenten

Player.svelte (Technikwahl, Tasten, SponsorBlock, Untertitel, Qualität, Ton, Cast), SegmentBar.svelte, LiveChat.svelte, Comments.svelte + CommentReplies.svelte, Paged.svelte (Liste mit Nachladen), VideoCard, ChannelCard, PlaylistCard, QueuePanel, SaveMenu, SubscribeButton, HideChannelButton, HomeGreeting (Startseite: Maskottchen-Röhrenfernseher als SVG – schaltet ein, blinzelt, wackelt mit den Antennen, beim Antippen erneut –, Schriftzug mit Röhren-Einschalteffekt und Spruch; reines CSS, läuft einmal, respektiert „weniger Bewegung“), ImportExport, Sidebar, Icon, Logo.

Rust

DateiSymbole
lib.rsrun, Befehle log, startup_options, open_text_file, save_text_file
media.rshandle, client_for, allowed, bound_ip
lan.rscast_serve, cast_unserve, rewrite_playlist, rewrite_mpd, with_cors, ensure_server, lan_ip; Stücklager cast_live_open, cast_live_put/live_put, cast_live_finish, cast_live_close, build_live_mpd, prune_live
cast.rscast_devices, cast_start, cast_status, cast_control, run_session, encode_message, decode_message

21. Lizenzen

Alle Bausteine sind freizügig lizenziert: YouTube.js, googlevideo, BgUtils (MIT), shaka-player (Apache-2.0), Tauri (MIT/Apache-2.0), Svelte/SvelteKit (MIT), axum/tokio (MIT), rustls (Apache-2.0/MIT/ISC), mdns-sd (MIT/Apache-2.0). Die Daten von SponsorBlock und DeArrow stehen unter CC BY-NC-SA 4.0: in einer kostenlosen App mit Namensnennung (steht auf der Seite „Über Flimmer“) in Ordnung, für eine kostenpflichtige App braucht es die Erlaubnis des Betreibers. Der SABR-Adapter (sabr-adapter.ts) ist aus dem MIT-lizenzierten Beispiel googlevideo/examples/sabr-shaka-example (LuanRT) angepasst. Die Liste in der App pflegt about.ts. Eine Lizenz für Flimmer selbst ist noch nicht festgelegt.

22. Offene Punkte

Doku pflegen

ANLEITUNG.md und DEVELOPERS.md sind die Quellen. Die HTML-Fassungen entstehen mit:

bash
deno run -A tools/doku-html.ts

Danach beide HTML-Dateien als Artifact neu veröffentlichen. Markdown, HTML und Artifact immer zusammen aktualisieren.