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:
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 {
|
||||
|
||||
Reference in New Issue
Block a user