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.")
}
/// 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 {