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