Fragen statt starten — die Probe überfuhr den Server

Beim letzten Lauf scheiterte auch die Anfrage, die vorher funktioniert
hatte. Kein neuer Befund, sondern mein Messfehler: die Probe warf sechs
Transcode-Sitzungen in Folge an, und danach lehnt Plex alles mit nackten
400ern ab — auch das, was vorher ging.

Jede Anfrage an `start.m3u8` startet einen Transcoder. Für eine
Parameterreihe ist das das falsche Werkzeug. Der **Entscheider** sagt,
was der Server täte, ohne etwas zu tun: Auflösung, Bitrate, und ob
umgerechnet oder umgepackt wird. Die Reihe läuft jetzt darüber.

Die zwei verbleibenden echten Sitzungen bekommen Abstand voneinander.

Der Wert wird aus der Antwort gesucht statt geparst — Plex antwortet je
nach Kopfzeile als XML oder JSON, und es geht um eine Anzeige, nicht um
Daten, mit denen weitergerechnet wird.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-27 18:15:05 +02:00
co-authored by Claude Opus 5
parent b861ca0599
commit d2237c29cd
2 changed files with 60 additions and 8 deletions
@@ -204,18 +204,26 @@ if CommandLine.arguments.contains("--leiter") {
resolution: auflösung, resolution: auflösung,
isLocal: connection.local isLocal: connection.local
) )
guard let url = server.transcodeURL(for: probe) else { continue } // **Über den Entscheider, nicht über start.m3u8.** Jede Anfrage dort
let antwort = await prüfePlaylist(url) // wirft einen Transcoder an; sechs in Folge bringen den Server dazu,
let zeile = antwort // auch das abzulehnen, was vorher ging. Der Entscheider sagt, was er
.split(separator: "\n") // täte, ohne etwas zu tun.
.first { $0.contains("Mbit/s") } let wortlaut = await server.decisionText(for: probe)
.map { $0.trimmingCharacters(in: .whitespaces) } ?? "keine Stufe" let auflösung = herausziehen("videoResolution", aus: wortlaut)
print(" \(name): \(zeile)") ?? herausziehen("height", aus: wortlaut)
await server.stopTranscode(sessionIdentifier: probe.sessionIdentifier) let bitrate = herausziehen("bitrate", aus: wortlaut)
let entscheidung = herausziehen("transcodeDecisionText", aus: wortlaut)
?? herausziehen("generalDecisionText", aus: wortlaut)
print(" \(name):")
print(" Auflösung \(auflösung ?? "–") · Bitrate \(bitrate ?? "–")")
if let entscheidung { print(" \(entscheidung)") }
} }
print() print()
// Zwischen den beiden echten Sitzungen Luft lassen: Plex nimmt eine
// schnelle Folge von Transcode-Starts übel und lehnt danach alles ab.
for (name, anfrage) in [("ohne autoAdjustQuality", ohne), ("mit autoAdjustQuality=1", mit)] { for (name, anfrage) in [("ohne autoAdjustQuality", ohne), ("mit autoAdjustQuality=1", mit)] {
try? await Task.sleep(for: .seconds(3))
print("=== \(name) ===") print("=== \(name) ===")
guard let url = server.transcodeURL(for: anfrage) else { guard let url = server.transcodeURL(for: anfrage) else {
print(" Keine Adresse zu bauen.\n") print(" Keine Adresse zu bauen.\n")
@@ -321,3 +329,26 @@ func storedClientIdentifier() -> String {
UserDefaults.standard.set(created, forKey: key) UserDefaults.standard.set(created, forKey: key)
return created return created
} }
/// Zieht einen Wert aus der Antwort des Entscheiders.
///
/// Plex antwortet je nach Kopfzeile als XML oder JSON; beide Formen tragen
/// den Wert als `name="wert"` bzw. `"name":"wert"`. Statt zwei Parser zu
/// bauen, wird hier gesucht — es geht um eine Anzeige, nicht um Daten, mit
/// denen weitergerechnet wird.
func herausziehen(_ name: String, aus text: String) -> String? {
for muster in ["\(name)=\"", "\"\(name)\":\""] {
guard let bereich = text.range(of: muster) else { continue }
let rest = text[bereich.upperBound...]
let wert = String(rest.prefix { $0 != "\"" })
if !wert.isEmpty { return wert }
}
// JSON-Zahlen stehen ohne Anführungszeichen.
if let bereich = text.range(of: "\"\(name)\":") {
let rest = text[bereich.upperBound...].drop { $0 == " " }
let wert = String(rest.prefix { $0.isNumber })
if !wert.isEmpty { return wert }
}
return nil
}
@@ -119,6 +119,27 @@ extension PlexServer {
) )
} }
/// Die Entscheidung im Wortlaut — **ohne** etwas zu starten.
///
/// Der Entscheider sagt, was der Server *täte*: welche Auflösung, welche
/// Bitrate, ob umgerechnet oder umgepackt wird. Das ist das richtige
/// Werkzeug, um Parameter auszuprobieren.
///
/// Der Weg über `start.m3u8` ist es **nicht**: jede Anfrage dort wirft
/// einen Transcoder an, und ein halbes Dutzend davon in Folge bringt den
/// Server dazu, alles Weitere mit nackten 400ern abzulehnen — auch das,
/// was vorher ging. Genau so ist eine Messung schon einmal gekippt.
public func decisionText(for request: TranscodeRequest) async -> String {
let daten = try? await client.send(
PlexRequest(
path: "/video/:/transcode/universal/decision",
query: request.queryItems + clientProfileItems
)
)
guard let daten else { return "nicht erreichbar" }
return String(decoding: daten.prefix(4_000), as: UTF8.self)
}
/// Hält den Transcoder an. /// Hält den Transcoder an.
/// ///
/// Muss beim Verlassen des Players laufen. Ohne dieses Signal rechnet der /// Muss beim Verlassen des Players laufen. Ohne dieses Signal rechnet der