From 314c1a3bfc184280f431dbbbc8d2eecba3a3949e Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sun, 27 Sep 2026 17:37:14 +0200 Subject: [PATCH] =?UTF-8?q?Die=20Schnipsel=20wiegen,=20nicht=20die=20Playl?= =?UTF-8?q?ist=20z=C3=A4hlen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Berechtigter Einwand: eine Playlist zu bekommen beweist gar nichts. Sie ist Text und kostet ein Kilobyte; die Datenrate steckt in den Schnipseln. Die vorige Fassung zählte `#EXTINF`-Zeilen und hätte „funktioniert" gemeldet, auch wenn der Server bei jeder Stufe denselben Strom ausliefert. Jetzt lädt die Probe je Stufe den **ersten Schnipsel** und rechnet aus, was er an Datenrate bedeutet — inklusive der unveränderten Stufe als Vergleichsmass. Werden sie kleiner, trägt die Idee. Bleiben sie gleich gross, rechnet der Server trotz anderer Zahl dasselbe, und ein selbstgebautes Menü brächte nichts. Co-Authored-By: Claude Opus 5 --- .../FlixyKit/Sources/FlixyProbe/Leiter.swift | 79 +++++++++++++------ 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/Packages/FlixyKit/Sources/FlixyProbe/Leiter.swift b/Packages/FlixyKit/Sources/FlixyProbe/Leiter.swift index eb76bff..7b2e9ef 100644 --- a/Packages/FlixyKit/Sources/FlixyProbe/Leiter.swift +++ b/Packages/FlixyKit/Sources/FlixyProbe/Leiter.swift @@ -358,20 +358,22 @@ func erklärungWarumKeineLeiter() { print(" sagt die Messung nichts darüber, was von unterwegs käme.") } -/// Prüft, ob wir uns die Leiter **selbst** bauen könnten. +/// Prüft, ob wir uns die Leiter **selbst** bauen könnten — und ob das etwas +/// brächte. /// -/// Eine Master-Playlist ist nur ein Menü: eine Liste von Adressen mit Angaben -/// dazu. Jellyfins eigene Leiter besteht aus derselben `main.m3u8`-Adresse, -/// nur mit anderem `VideoBitrate`. Liefert der Server diese Unter-Playlists -/// auch dann, wenn **wir** die Zahl ändern, dann braucht es seine Zustimmung -/// gar nicht — dann schreiben wir das Menü. +/// Eine Master-Playlist ist nur ein Menü: eine Liste von Adressen. Jellyfins +/// eigene Leiter bestünde aus derselben `main.m3u8`-Adresse, mehrfach, mit +/// anderem `VideoBitrate`. Schreiben wir das Menü selbst, brauchen wir seine +/// Zustimmung nicht. /// -/// Zusätzlich mit `maxHeight`: Jellyfins eigene Leiter lässt die Auflösung -/// stehen und dreht nur an der Bitrate. Eine Stufe, die auch kleiner rechnet, -/// sieht bei wenig Bandbreite deutlich besser aus. +/// **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 +/// läuft. Deshalb wird hier gewogen, nicht gezählt. func prüfeSelbstgebauteLeiter(master: URL, kopfzeilen: [String: String]) async { print("") - print("=== Ginge eine selbstgebaute Leiter? ===") + print("=== Ginge eine selbstgebaute Leiter — und brächte sie etwas? ===") guard let (daten, _) = try? await URLSession.shared.data(for: anfrage(master, kopfzeilen)), let zeile = String(decoding: daten, as: UTF8.self) @@ -383,29 +385,60 @@ func prüfeSelbstgebauteLeiter(master: URL, kopfzeilen: [String: String]) async return } - let versuche: [(String, Int, Int?)] = [ + let versuche: [(String, Int?, Int?)] = [ + ("wie gehabt", nil, nil), ("halbe Bitrate", 3_000_000, nil), ("ein Viertel", 1_500_000, nil), ("ein Viertel + 480p", 1_500_000, 480), ] for (name, bitrate, höhe) in versuche { - var ziel = setzend("VideoBitrate", auf: String(bitrate), in: unter) + var ziel = unter + if let bitrate { ziel = setzend("VideoBitrate", auf: String(bitrate), in: ziel) } if let höhe { ziel = setzend("maxHeight", auf: String(höhe), in: ziel) } - - if let (roh, antwort) = try? await URLSession.shared.data(for: anfrage(ziel, kopfzeilen)) { - let status = (antwort as? HTTPURLResponse)?.statusCode ?? 0 - let text = String(decoding: roh, as: UTF8.self) - let schnipsel = text.split(separator: "\n").count { $0.hasPrefix("#EXTINF") } - print(" \(name): HTTP \(status), \(schnipsel) Schnipsel") - } else { - print(" \(name): nicht erreichbar") - } + print(" \(name): \(await wiegeErstenSchnipsel(ziel, kopfzeilen: kopfzeilen))") } print("") - print(" Kommen hier überall Schnipsel zurück, brauchen wir Jellyfins") - print(" Zustimmung nicht — dann schreiben wir das Menü selbst.") + print(" Werden die Schnipsel kleiner, trägt die Idee. Bleiben sie gleich") + print(" gross, rechnet der Server trotz anderer Zahl denselben Strom —") + print(" dann bringt ein selbstgebautes Menü nichts.") +} + +/// Holt die Unter-Playlist, lädt den ersten Schnipsel und rechnet aus, was das +/// an Datenrate bedeutet. +private func wiegeErstenSchnipsel(_ playlist: URL, kopfzeilen: [String: String]) async -> String { + guard let (daten, antwort) = try? await URLSession.shared.data(for: anfrage(playlist, kopfzeilen)) else { + return "nicht erreichbar" + } + let status = (antwort as? HTTPURLResponse)?.statusCode ?? 0 + guard (200..<300).contains(status) else { return "HTTP \(status)" } + + let zeilen = String(decoding: daten, as: UTF8.self).split(separator: "\n") + guard let dauerZeile = zeilen.firstIndex(where: { $0.hasPrefix("#EXTINF") }), + zeilen.indices.contains(dauerZeile + 1), + let schnipselURL = URL(string: String(zeilen[dauerZeile + 1]), relativeTo: playlist)?.absoluteURL + else { return "HTTP \(status), aber kein Schnipsel darin" } + + let dauer = Double( + zeilen[dauerZeile] + .dropFirst("#EXTINF:".count) + .prefix { $0 != "," } + ) ?? 0 + + guard let (rohdaten, schnipselAntwort) = try? await URLSession.shared.data( + for: anfrage(schnipselURL, kopfzeilen) + ) else { return "Playlist ja, Schnipsel nicht erreichbar" } + + let schnipselStatus = (schnipselAntwort as? HTTPURLResponse)?.statusCode ?? 0 + guard (200..<300).contains(schnipselStatus) else { + return "Playlist ja, Schnipsel HTTP \(schnipselStatus)" + } + + let kilobyte = Double(rohdaten.count) / 1024 + guard dauer > 0 else { return String(format: "%.0f kB", kilobyte) } + let kilobit = Double(rohdaten.count) * 8 / 1000 / dauer + return String(format: "%.0f kB für %.1f s → %.0f kbit/s", kilobyte, dauer, kilobit) } private func anfrage(_ url: URL, _ kopfzeilen: [String: String]) -> URLRequest {