Die Schnipsel wiegen, nicht die Playlist zählen

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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-27 17:37:14 +02:00
co-authored by Claude Opus 5
parent f48eb55f35
commit 314c1a3bfc
@@ -358,20 +358,22 @@ func erklärungWarumKeineLeiter() {
print(" sagt die Messung nichts darüber, was von unterwegs käme.") 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 /// Eine Master-Playlist ist nur ein Menü: eine Liste von Adressen. Jellyfins
/// dazu. Jellyfins eigene Leiter besteht aus derselben `main.m3u8`-Adresse, /// eigene Leiter bestünde aus derselben `main.m3u8`-Adresse, mehrfach, mit
/// nur mit anderem `VideoBitrate`. Liefert der Server diese Unter-Playlists /// anderem `VideoBitrate`. Schreiben wir das Menü selbst, brauchen wir seine
/// auch dann, wenn **wir** die Zahl ändern, dann braucht es seine Zustimmung /// Zustimmung nicht.
/// gar nicht — dann schreiben wir das Menü.
/// ///
/// Zusätzlich mit `maxHeight`: Jellyfins eigene Leiter lässt die Auflösung /// **Die Playlist zu bekommen beweist aber noch nichts.** Sie ist Text und
/// stehen und dreht nur an der Bitrate. Eine Stufe, die auch kleiner rechnet, /// kostet nichts; die Datenrate steckt in den Schnipseln. Entscheidend ist
/// sieht bei wenig Bandbreite deutlich besser aus. /// 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 { func prüfeSelbstgebauteLeiter(master: URL, kopfzeilen: [String: String]) async {
print("") 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)), guard let (daten, _) = try? await URLSession.shared.data(for: anfrage(master, kopfzeilen)),
let zeile = String(decoding: daten, as: UTF8.self) let zeile = String(decoding: daten, as: UTF8.self)
@@ -383,29 +385,60 @@ func prüfeSelbstgebauteLeiter(master: URL, kopfzeilen: [String: String]) async
return return
} }
let versuche: [(String, Int, Int?)] = [ let versuche: [(String, Int?, Int?)] = [
("wie gehabt", nil, nil),
("halbe Bitrate", 3_000_000, nil), ("halbe Bitrate", 3_000_000, nil),
("ein Viertel", 1_500_000, nil), ("ein Viertel", 1_500_000, nil),
("ein Viertel + 480p", 1_500_000, 480), ("ein Viertel + 480p", 1_500_000, 480),
] ]
for (name, bitrate, höhe) in versuche { 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 höhe { ziel = setzend("maxHeight", auf: String(höhe), in: ziel) }
print(" \(name): \(await wiegeErstenSchnipsel(ziel, kopfzeilen: kopfzeilen))")
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("") print("")
print(" Kommen hier überall Schnipsel zurück, brauchen wir Jellyfins") print(" Werden die Schnipsel kleiner, trägt die Idee. Bleiben sie gleich")
print(" Zustimmung nicht — dann schreiben wir das Menü selbst.") 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 { private func anfrage(_ url: URL, _ kopfzeilen: [String: String]) -> URLRequest {