diff --git a/Flixy/Resources/Localizable.xcstrings b/Flixy/Resources/Localizable.xcstrings index 17025d0..c6b3018 100644 --- a/Flixy/Resources/Localizable.xcstrings +++ b/Flixy/Resources/Localizable.xcstrings @@ -3499,7 +3499,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Es kommen keine Daten mehr. Der Strom ist abgerissen." + "value" : "Es kommen keine Daten mehr. Der Stream ist abgerissen." } }, "en" : { @@ -3510,6 +3510,23 @@ } } }, + "player.stalledUnlimited" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es kommen keine Daten mehr. Die Qualität steht auf „Original\", der Server überträgt also die Datei unverändert — dafür reicht diese Verbindung nicht. Unter Einstellungen → Wiedergabe eine kleinere Stufe wählen." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No more data is arriving. Quality is set to “Original”, so the server sends the file unchanged — this connection cannot carry that. Pick a smaller step under Settings → Playback." + } + } + } + }, "player.stop" : { "localizations" : { "de" : { @@ -3768,7 +3785,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Dieser Strom hat nur eine Stufe — es gibt nichts, worauf der Player ausweichen könnte. Bei Rücklern hilft hier nur eine kleinere Qualität von Hand." + "value" : "Dieser Stream hat nur eine Stufe — es gibt nichts, worauf der Player ausweichen könnte. Bei Rücklern hilft hier nur eine kleinere Qualität von Hand." } }, "en" : { diff --git a/Flixy/Views/Player/PlayerModel.swift b/Flixy/Views/Player/PlayerModel.swift index ed64410..926c82c 100644 --- a/Flixy/Views/Player/PlayerModel.swift +++ b/Flixy/Views/Player/PlayerModel.swift @@ -91,7 +91,7 @@ final class PlayerModel { /// Die eingestellte Stufe in zwei bis fünf Zeichen. /// /// Steht auf dem Knopf im Player. „Automatisch" heißt dabei: was gerade - /// tatsächlich ankommt — bei einem HLS-Strom entscheidet der Player selbst, + /// tatsächlich ankommt — bei einem HLS-Stream entscheidet der Player selbst, /// und ihn danach zu fragen ist ehrlicher, als „Auto" hinzuschreiben und /// die Antwort schuldig zu bleiben. var qualityLabel: String { @@ -177,7 +177,7 @@ final class PlayerModel { /// Wacht darüber, dass aus „lädt" nicht „lädt für immer" wird. @ObservationIgnored private var bufferWatchdog: Task? - /// Wie viel Vorrat bei einem Strom über das Netz angelegt wird. + /// Wie viel Vorrat bei einem Stream über das Netz angelegt wird. /// /// **Nur für Filme und Folgen, nicht für Sender und nicht für Dateien.** /// Ein laufender Sender soll nah am Jetzt bleiben — ein halbminütiger @@ -190,7 +190,7 @@ final class PlayerModel { /// gedrosselten Leitung ermittelt. private static let forwardBuffer: Duration = .seconds(25) - /// Wie oft der Zustand des Stroms geprüft wird. + /// Wie oft der Zustand des Streams geprüft wird. /// /// Einmal je Sekunde: fein genug, dass ein schrumpfender Puffer auffällt, /// bevor er leer ist, und grob genug, dass es nichts kostet. @@ -202,7 +202,7 @@ final class PlayerModel { /// kostet Schärfe, ein falscher nach oben einen Rückler. private static let stepUpCooldown: Duration = .seconds(60) - /// Der Zustand des Stroms, fortlaufend beurteilt. + /// Der Zustand des Streams, fortlaufend beurteilt. @ObservationIgnored private var health = StreamHealth() @ObservationIgnored private var healthTask: Task? /// Wann zuletzt eine Stufe höher gegangen wurde. @@ -357,7 +357,7 @@ final class PlayerModel { // Die zuletzt gewählte Stufe, falls sie gemerkt werden soll. **Vor** // der Entscheidung, nicht danach: hinterher umzustellen hieße, den - // Strom zweimal aufzubauen und den Anfang zweimal zu puffern. + // Stream zweimal aufzubauen und den Anfang zweimal zu puffern. if let stored = settings.rememberedQuality, let restored = PlayerQuality.restored(from: stored, forSourceHeight: media.height) { quality = restored @@ -406,7 +406,7 @@ final class PlayerModel { await restart() } - /// Baut den Strom mit der **aktuell geltenden** Richtlinie neu auf. + /// Baut den Stream mit der **aktuell geltenden** Richtlinie neu auf. /// /// Geteilt zwischen der Wahl von Hand und der Sicherheitsleine — der /// Ablauf ist derselbe, nur der Anlass nicht. Was sich unterscheidet, @@ -549,7 +549,7 @@ final class PlayerModel { diagnostics.note("Engine gestartet") // Weder offline noch live gibt es einen Fortschritt zu melden. if !isOffline, !isLive { startReporting() } - // Den Zustand des Stroms dagegen auch bei Live beobachten: dort + // Den Zustand des Streams dagegen auch bei Live beobachten: dort // hakt es am ehesten, und der billige Hebel kostet nichts. startHealthWatch() } catch { @@ -617,7 +617,7 @@ final class PlayerModel { // MARK: - Die Qualität selbst nachregeln - /// Beobachtet den Strom und passt die Obergrenze an. + /// Beobachtet den Stream und passt die Obergrenze an. /// /// Läuft nur bei „automatisch". Eine ausdrücklich gewählte Stufe ist eine /// Ansage und wird nicht überstimmt — `automatic` ist die Erlaubnis @@ -662,7 +662,7 @@ final class PlayerModel { // MARK: - Die Sicherheitsleine - /// Prüft, ob der Strom neu aufgebaut werden muss — und tut es. + /// Prüft, ob der Stream neu aufgebaut werden muss — und tut es. private func considerTheHardWay( _ condition: StreamCondition, statistics: PlaybackStatistics, @@ -699,7 +699,7 @@ final class PlayerModel { } } - /// Baut den Strom mit kleinerer Bitrate neu auf. + /// Baut den Stream mit kleinerer Bitrate neu auf. private func downgradeTheHardWay(at now: Duration) async { guard let ziel = nextLowerRung() else { hasGivenUp = true @@ -719,6 +719,31 @@ final class PlayerModel { } } + /// Ein zweiter Versuch mit kleinerer Bitrate, bevor aufgegeben wird. + /// + /// Gibt `true` zurück, wenn es einen gab. Zählt gegen dasselbe Budget wie + /// die Sicherheitsleine — sonst könnte eine tote Leitung die App endlos + /// im Kreis schicken. + private func retrySmaller() async -> Bool { + guard quality == .automatic, + downgradesUsed < downgradeBudget, + let ziel = nextLowerRung() + else { return false } + + automaticCeiling = ziel + downgradesUsed += 1 + diagnostics.note("Nichts kam an — neuer Versuch mit höchstens \(ziel) kbit") + + if isLive { + await retuneLive(to: ziel) + } else { + await restart() + } + // Ist der Neuaufbau selbst gescheitert, steht die Meldung schon da. + if case .failed = phase { return false } + return true + } + /// Die nächste Sprosse unterhalb der aktuellen — aus **unserer** Leiter. /// /// Nicht aus der Playlist: die hat in diesem Fall gerade keine, sonst @@ -914,11 +939,33 @@ final class PlayerModel { try? await Task.sleep(for: .seconds(40)) guard !Task.isCancelled, let self, self.isBuffering else { return } - diagnostics.note("Seit 40 s keine Daten — abgebrochen") + diagnostics.note("Seit 40 s keine Daten") + // **Auch das ist ein Beleg**, und ein stärkerer als ein paar + // Rückler: hier kam überhaupt nichts an. Ohne diese Zeile bliebe + // der Hinweis in den Einstellungen aus, weil das Rücklerbuch erst + // ab dem ersten Bild zählt — und das gab es nie. + settings.lastPlaybackStruggled = true + + // **Erst kleiner versuchen, dann aufgeben.** + // + // Hier lag der eigentliche Fehler: die ganze Regelung misst ab + // dem ersten Bild. Kommt nie eines, misst sie nichts, greift + // nichts, und „Auto" hat sich als das erwiesen, was es hiess — + // „nimm, was in den Einstellungen steht". Steht dort „Original", + // bittet die App über eine Zugverbindung um eine 4K-Datei und + // wartet vierzig Sekunden auf etwas, das nie kommen kann. + if await self.retrySmaller() { return } // Den Engine gleich stilllegen: er würde sonst weiter an einem // toten Stream ziehen, und bei Live-TV bliebe der Tuner belegt. silence() - phase = .failed(message: String(localized: "player.stalled")) + // Wer ohne Grenze angefragt hat, bekommt den Grund dazu. „Der + // Stream ist abgerissen" ist wahr und führt nirgendwohin; die + // Einstellung zu nennen ist der Unterschied zwischen einer + // Meldung und einer Hilfe. + let ohneGrenze = quality == .automatic && effectivePolicy.maximumBitrate == nil + phase = .failed(message: String( + localized: ohneGrenze ? "player.stalledUnlimited" : "player.stalled" + )) Task { await self.releaseOnServer() } } } diff --git a/Packages/FlixyKit/Sources/FlixyPlayback/Decision/StallLedger.swift b/Packages/FlixyKit/Sources/FlixyPlayback/Decision/StallLedger.swift index 37deacb..5489509 100644 --- a/Packages/FlixyKit/Sources/FlixyPlayback/Decision/StallLedger.swift +++ b/Packages/FlixyKit/Sources/FlixyPlayback/Decision/StallLedger.swift @@ -6,7 +6,7 @@ import Foundation /// es seit vierzig Sekunden am Stück? Bei jedem kurzen Erholen setzte er sich /// zurück — zehn Rückler à dreißig Sekunden lösten nichts aus, obwohl die /// halbe Stunde unbrauchbar war. Genau dieser Fall ist der, über den geklagt -/// wird: nicht der tote Strom, sondern der, der immer wieder hakt. +/// wird: nicht der tote Stream, sondern der, der immer wieder hakt. /// /// **Warum als eigener Typ.** Keine Uhr, kein AVFoundation, kein Netz — die /// Zeiten kommen von außen herein. Damit lässt sich eine Folge von Rücklern diff --git a/Packages/FlixyKit/Sources/FlixyPlayback/Decision/StreamHealth.swift b/Packages/FlixyKit/Sources/FlixyPlayback/Decision/StreamHealth.swift index df3f4b1..af9955f 100644 --- a/Packages/FlixyKit/Sources/FlixyPlayback/Decision/StreamHealth.swift +++ b/Packages/FlixyKit/Sources/FlixyPlayback/Decision/StreamHealth.swift @@ -1,6 +1,6 @@ import Foundation -/// Eine Probe vom laufenden Strom. +/// Eine Probe vom laufenden Stream. /// /// Alle Zeiten **seit Beginn dieser Wiedergabe**, nicht als Datum — dasselbe /// wie beim `StallLedger`, und aus demselben Grund: eine Systemuhr, die sich @@ -31,7 +31,7 @@ public struct StreamSample: Sendable, Equatable { } } -/// Wie es dem Strom geht. +/// Wie es dem Stream geht. public enum StreamCondition: Sendable, Equatable { /// Trägt. Es darf wieder eine Stufe höher gehen. case healthy @@ -41,7 +41,7 @@ public enum StreamCondition: Sendable, Equatable { case failing } -/// Beurteilt den Strom aus einer Folge von Proben. +/// Beurteilt den Stream aus einer Folge von Proben. /// /// **Warum als reiner Typ.** Kein AVFoundation, keine Uhr, kein Netz — Proben /// hinein, Urteil heraus. Dieselbe Bauform wie `PlaybackDecider`, und aus diff --git a/Packages/FlixyKit/Sources/FlixyPlayback/Engine/AVPlayerEngine.swift b/Packages/FlixyKit/Sources/FlixyPlayback/Engine/AVPlayerEngine.swift index 747f8fc..c26f1cc 100644 --- a/Packages/FlixyKit/Sources/FlixyPlayback/Engine/AVPlayerEngine.swift +++ b/Packages/FlixyKit/Sources/FlixyPlayback/Engine/AVPlayerEngine.swift @@ -76,7 +76,7 @@ public final class AVPlayerEngine: VideoPlayerEngine { /// /// Aus `playbackStalledNotification` und **nicht** aus dem /// Zugriffsprotokoll: die Meldung kommt bei jeder Quelle, das Protokoll - /// nur bei Strömen über das Netz. Beides zu addieren zählte doppelt. + /// nur bei Streamsn über das Netz. Beides zu addieren zählte doppelt. private var stallCount = 0 /// Aufsummierte Wartezeit. Siehe `noteStall(from:to:)`. private var accumulatedStall: Duration = .zero diff --git a/Packages/FlixyKit/Sources/FlixyPlayback/Engine/VideoPlayerEngine.swift b/Packages/FlixyKit/Sources/FlixyPlayback/Engine/VideoPlayerEngine.swift index d8d1fdd..bd3c2bf 100644 --- a/Packages/FlixyKit/Sources/FlixyPlayback/Engine/VideoPlayerEngine.swift +++ b/Packages/FlixyKit/Sources/FlixyPlayback/Engine/VideoPlayerEngine.swift @@ -17,7 +17,7 @@ public struct PlaybackSource: Sendable, Equatable { /// Wie viel Vorrat der Player anlegen soll. /// /// `nil` überlässt es dem System. Das ist für eine Datei auf der Platte - /// richtig und für einen Sender richtig — bei einem Strom über eine + /// richtig und für einen Sender richtig — bei einem Stream über eine /// wackelige Leitung ist es zu wenig: der Systemwert ist auf schnellen /// Start ausgelegt, nicht darauf, einen Aussetzer zu überstehen. /// diff --git a/Packages/FlixyKit/Sources/FlixyProbe/Leiter.swift b/Packages/FlixyKit/Sources/FlixyProbe/Leiter.swift index 7b2e9ef..bee29b0 100644 --- a/Packages/FlixyKit/Sources/FlixyProbe/Leiter.swift +++ b/Packages/FlixyKit/Sources/FlixyProbe/Leiter.swift @@ -369,7 +369,7 @@ func erklärungWarumKeineLeiter() { /// **Die Playlist zu bekommen beweist aber noch nichts.** Sie ist Text und /// kostet nichts; die Datenrate steckt in den Schnipseln. Entscheidend ist /// deshalb allein, ob die Schnipsel bei kleinerer Zahl auch kleiner werden — -/// oder ob der Server stur denselben Strom ausliefert, weil die Sitzung schon +/// oder ob der Server stur denselben Stream ausliefert, weil die Sitzung schon /// läuft. Deshalb wird hier gewogen, nicht gezählt. func prüfeSelbstgebauteLeiter(master: URL, kopfzeilen: [String: String]) async { print("") @@ -401,7 +401,7 @@ func prüfeSelbstgebauteLeiter(master: URL, kopfzeilen: [String: String]) async print("") print(" Werden die Schnipsel kleiner, trägt die Idee. Bleiben sie gleich") - print(" gross, rechnet der Server trotz anderer Zahl denselben Strom —") + print(" gross, rechnet der Server trotz anderer Zahl denselben Stream —") print(" dann bringt ein selbstgebautes Menü nichts.") } diff --git a/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend+LiveTV.swift b/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend+LiveTV.swift index 5dc619f..ca39b55 100644 --- a/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend+LiveTV.swift +++ b/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend+LiveTV.swift @@ -130,7 +130,7 @@ extension JellyfinBackend: LiveTVBackend { } // Kein Transcoder-Weg: dann unverändert. Manche Empfänger liefern - // bereits einen Strom, den das Gerät ohne Umrechnung versteht. + // bereits einen Stream, den das Gerät ohne Umrechnung versteht. if let url = client.url( path: "/Videos/\(channel.id)/stream", query: [ diff --git a/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend+Playback.swift b/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend+Playback.swift index 7eff6ff..35ebb60 100644 --- a/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend+Playback.swift +++ b/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend+Playback.swift @@ -164,7 +164,7 @@ extension JellyfinBackend { URLQueryItem(name: "maxStreamingBitrate", value: String(bitsPerSecond)), // **Kein** `startTimeTicks`. Es gäbe zwei Stellen, die // über den Startpunkt entscheiden: der Server ließe den - // Strom dort beginnen, und der Player spränge anschließend + // Stream dort beginnen, und der Player spränge anschließend // noch einmal um dieselbe Strecke vor — bei einundzwanzig // Minuten Fortschritt landete man bei zweiundvierzig. // Gesprungen wird an genau einer Stelle, und das ist der diff --git a/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend.swift b/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend.swift index a3570b7..83fff09 100644 --- a/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend.swift +++ b/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend.swift @@ -302,7 +302,7 @@ public struct JellyfinBackend: MediaBackend { /// Eine verkleinerte Fassung zum Herunterladen. /// /// **Fortlaufendes MP4, nicht HLS.** Zum Abspielen liefert Jellyfin eine - /// Abspielliste mit hunderten Schnipseln; das ist für einen Strom richtig + /// Abspielliste mit hunderten Schnipseln; das ist für einen Stream richtig /// und für eine Datei, die auf dem Gerät liegen soll, unbrauchbar. Der /// Endpunkt `/Videos/{id}/stream.mp4` gibt dieselbe Umrechnung als **eine** /// Datei heraus. diff --git a/Packages/FlixyKit/Sources/MediaKit/MediaPlayback.swift b/Packages/FlixyKit/Sources/MediaKit/MediaPlayback.swift index 5604a95..0d9e657 100644 --- a/Packages/FlixyKit/Sources/MediaKit/MediaPlayback.swift +++ b/Packages/FlixyKit/Sources/MediaKit/MediaPlayback.swift @@ -46,7 +46,7 @@ public struct TranscodeSettings: Sendable, Equatable { /// Ob der Server eine **Leiter** liefern soll statt einer festen Stufe. /// /// Dann enthält die Playlist mehrere Varianten, und der Player wechselt - /// zwischen ihnen, ohne dass der Strom neu aufgebaut werden muss. Die + /// zwischen ihnen, ohne dass der Stream neu aufgebaut werden muss. Die /// angegebene Bitrate bleibt dabei die **oberste** Sprosse; was darunter /// liegt, erfindet der Server. /// diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift index 3adc640..e15980a 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift @@ -1010,7 +1010,7 @@ extension PlexServer { return nil } - /// Baut eine Stromadresse und ruft sie einmal ab. + /// Baut eine Stream-Adresse und ruft sie einmal ab. /// /// Getrennte Methode statt einer Hilfsfunktion in `tune`: eine solche /// Funktion greift über jede Unterbrechung hinweg auf die veränderlichen @@ -1076,7 +1076,7 @@ extension PlexServer { } } - /// Holt eine Stromadresse einmal selbst ab und sagt, was zurückkam. + /// Holt eine Stream-Adresse einmal selbst ab und sagt, was zurückkam. /// /// **Ohne Plex-Kopfzeilen**, weil AVPlayer sie ebenfalls nicht schickt. /// Mit Kopfzeilen zu prüfen hieße, etwas anderes zu prüfen als das, was diff --git a/Packages/FlixyKit/Sources/PlexKit/PlexBackend.swift b/Packages/FlixyKit/Sources/PlexKit/PlexBackend.swift index 94e77bb..edd6243 100644 --- a/Packages/FlixyKit/Sources/PlexKit/PlexBackend.swift +++ b/Packages/FlixyKit/Sources/PlexKit/PlexBackend.swift @@ -179,7 +179,7 @@ public struct PlexBackend: MediaBackend { // Wiedergabe verhindert. Plex' eigene Clients schicken ihn; welche // weitere Angabe sie dabei mitgeben, ist offen. Bis das gemessen ist, // bleibt er draußen: eine Leiter, die es vielleicht gibt, ist keinen - // Strom wert, der sicher nicht läuft. + // Stream wert, der sicher nicht läuft. // // `TranscodeRequest.adaptive` bleibt samt Tests bestehen — das // Gerüst steht, es fehlt die richtige Beschwörungsformel. diff --git a/Packages/FlixyKit/Tests/FlixyPlaybackTests/DowngradeRuleTests.swift b/Packages/FlixyKit/Tests/FlixyPlaybackTests/DowngradeRuleTests.swift index d5672af..bc97701 100644 --- a/Packages/FlixyKit/Tests/FlixyPlaybackTests/DowngradeRuleTests.swift +++ b/Packages/FlixyKit/Tests/FlixyPlaybackTests/DowngradeRuleTests.swift @@ -73,7 +73,7 @@ struct DowngradeRuleTests { #expect(DowngradeRule.verdict(facts) == .downgrade) } - @Test("Ein gesunder Strom löst nichts aus") + @Test("Ein gesunder Stream löst nichts aus") func healthyNeverTriggers() { #expect(DowngradeRule.verdict(notlage(condition: .healthy)) == .notBadEnough) } diff --git a/Packages/FlixyKit/Tests/FlixyPlaybackTests/StreamHealthTests.swift b/Packages/FlixyKit/Tests/FlixyPlaybackTests/StreamHealthTests.swift index 297e6e3..ff14b3d 100644 --- a/Packages/FlixyKit/Tests/FlixyPlaybackTests/StreamHealthTests.swift +++ b/Packages/FlixyKit/Tests/FlixyPlaybackTests/StreamHealthTests.swift @@ -2,7 +2,7 @@ import Testing import Foundation @testable import FlixyPlayback -@Suite("Zustand des Stroms") +@Suite("Zustand des Streams") struct StreamHealthTests { /// Baut eine Folge von Proben im Sekundentakt. private func lauf( diff --git a/Packages/FlixyVLC/Sources/FlixyVLC/VLCPlayerEngine.swift b/Packages/FlixyVLC/Sources/FlixyVLC/VLCPlayerEngine.swift index ef6199b..3e81891 100644 --- a/Packages/FlixyVLC/Sources/FlixyVLC/VLCPlayerEngine.swift +++ b/Packages/FlixyVLC/Sources/FlixyVLC/VLCPlayerEngine.swift @@ -21,7 +21,7 @@ public final class VLCPlayerEngine: NSObject, VideoPlayerEngine { private let player = VLCMediaPlayer() - /// Was VLC über den laufenden Strom weiß. + /// Was VLC über den laufenden Stream weiß. /// /// Bisher meldete dieser Weg gar nichts: die Standardimplementierung gibt /// `nil` zurück, und die Bitratenanzeige blieb bei VLC dauerhaft leer. @@ -227,7 +227,7 @@ extension VLCPlayerEngine: VLCMediaPlayerDelegate { /// Aufzählung führt nur `opening`, `playing`, `paused`, `stopped` und /// `error`. Weil diese Methode bisher fehlte, ging der Zustand /// `.buffering` auf dem VLC-Weg **nie** an — und damit löste der - /// Watchdog im Player dort nie aus. Ein hängender Strom lief stumm ins + /// Watchdog im Player dort nie aus. Ein hängender Stream lief stumm ins /// Nichts, ohne Kreisel, ohne Meldung, ohne Abbruch. /// /// Nur aus dem Spielen heraus gewertet. Die Dokumentation sagt, dass vor diff --git a/web/README.md b/web/README.md index 6e60d31..4a4a2ab 100644 --- a/web/README.md +++ b/web/README.md @@ -19,7 +19,7 @@ davon, wie gut das Frontend geschrieben ist: - **Herkunft.** Anfragen laufen über `/api` und damit über dieselbe Adresse wie die Seite. Kein CORS, auch nicht beim Streamen. - **Klartext und Verschlüsselung.** Läuft der Medienserver über `http` und die - Seite über `https`, blockiert der Browser den Strom. Dagegen kann keine Zeile + Seite über `https`, blockiert der Browser den Stream. Dagegen kann keine Zeile im Frontend etwas tun — ein Server davor schon. Nebenbei bleibt der Zugangsschlüssel beim Server, statt im Browser zu liegen.