Live-TV: jeder Weg wird bis zum Ende geprüft, bevor der Player ihn bekommt
Wieder HTTP 400, und wieder ohne Begründung. Der Grund, dass das dreimal passieren konnte: eine gebaute Adresse galt schon als Erfolg. Ob der Server sie überhaupt annimmt, stellte sich erst im Player heraus — und dort steht nur „HTTP 400 (unhandled)". Jetzt ruft die App die Adresse einmal selbst ab, ohne Plex-Kopfzeilen, genau so wie AVPlayer es tut. Nur was mit 200 antwortet, geht an den Player. Was nicht, steht mit dem Wortlaut des Servers in der Spur. Weil weder der Schlüssel für die Sitzung noch die anschließend umzurechnende Adresse für diesen Teil der Plex-Schnittstelle beschrieben sind, werden die Kombinationen der Reihe nach durchprobiert — Adresse der Sitzung, Adresse aus der Antwort, Adresse des Teils darin, je mit mediaIndex -1 und 0, höchstens acht Versuche. Antwortet keiner, wird der Transcoder wieder angehalten, statt für nichts weiterzurechnen. Die Spur steht in einem Blatt statt in einem Dialog: die Antwort des Servers ist mehrzeilig, und ein Dialog schneidet sie ab — dann stünde dort wieder nur, dass es nicht ging. Dazu ein Nebeneffekt, der beim zweiten Sender zugeschlagen hätte: der Player kannte die Sitzungskennung des Live-Stroms nicht und hat den Tuner beim Schließen nicht freigegeben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8dba05b4f9
commit
a9a45e685d
@@ -364,14 +364,15 @@ extension PlexServer {
|
||||
///
|
||||
/// Zwei Schritte, und der erste ist der, den ich zuerst übersprungen
|
||||
/// hatte: **der Server muss den Tuner belegen**. Erst danach gibt es
|
||||
/// etwas umzurechnen. Ohne diesen Schritt begann die Wiedergabe und starb
|
||||
/// sofort wieder — „die Wiedergabe wurde unterbrochen", ohne dass ein
|
||||
/// Fehler zu sehen gewesen wäre.
|
||||
/// etwas umzurechnen.
|
||||
///
|
||||
/// Unter welchem Schlüssel die Sitzung eröffnet wird, unterscheidet sich
|
||||
/// je nach Anbieter, deshalb werden die in Frage kommenden der Reihe nach
|
||||
/// probiert. Was dabei herauskam, steht in `trace` — scheitert alles, ist
|
||||
/// ohne Rückfrage zu sehen, was der Server geantwortet hat.
|
||||
/// Weder der Schlüssel für die Sitzung noch die Adresse, die anschließend
|
||||
/// umzurechnen ist, sind für diesen Teil der Plex-Schnittstelle
|
||||
/// beschrieben. Deshalb wird jede Kombination **bis zum Ende geprüft**:
|
||||
/// die Adresse wird einmal selbst abgerufen, ohne Kopfzeilen — genau so,
|
||||
/// wie der Systemplayer es tut. Erst was dabei mit 200 antwortet, geht an
|
||||
/// den Player. Vorher galt schon eine gebaute Adresse als Erfolg, und der
|
||||
/// Fehler tauchte erst im Player auf, als „HTTP 400" ohne Begründung.
|
||||
public func tune(
|
||||
channel: PlexChannel,
|
||||
program: PlexProgram? = nil,
|
||||
@@ -379,75 +380,131 @@ extension PlexServer {
|
||||
) async -> LiveTuneResult {
|
||||
let session = UUID().uuidString
|
||||
var trace: [ChannelProbe] = []
|
||||
var attempts = 0
|
||||
|
||||
for key in Self.sessionKeys(channel: channel, program: program) {
|
||||
let path = "/livetv/sessions/\(session)/\(key)"
|
||||
let sessionPath = "/livetv/sessions/\(session)"
|
||||
let openPath = "\(sessionPath)/\(key)"
|
||||
|
||||
let item: PlexItem?
|
||||
do {
|
||||
let container = try await client.send(
|
||||
item = try await client.send(
|
||||
PlexRequest(
|
||||
path: path,
|
||||
path: openPath,
|
||||
query: [URLQueryItem(name: "X-Plex-Session-Identifier", value: session)],
|
||||
timeout: .seconds(30)
|
||||
),
|
||||
as: PlexMediaContainer<PlexItem>.self
|
||||
)
|
||||
|
||||
guard let item = container.content.first else {
|
||||
trace.append(ChannelProbe(path: path, result: "leer"))
|
||||
continue
|
||||
}
|
||||
|
||||
// Die Sitzung hat jetzt einen Tuner. Umgerechnet wird die
|
||||
// Adresse der **Sitzung** — nicht die des Senders, nicht die
|
||||
// der Sendung und nicht die des Teils darin. Der Transcoder
|
||||
// sucht sich den Teil selbst; deshalb steht unten auch -1.
|
||||
let sessionPath = "/livetv/sessions/\(session)"
|
||||
let streamPath = item.key.hasPrefix("/livetv/sessions/")
|
||||
? item.key
|
||||
: (item.media.first?.parts.first?.key ?? sessionPath)
|
||||
|
||||
trace.append(ChannelProbe(path: path, result: "Sitzung: \(streamPath)"))
|
||||
|
||||
return LiveTuneResult(
|
||||
url: transcodeURL(for: TranscodeRequest(
|
||||
ratingKey: item.ratingKey,
|
||||
// -1 heißt „was auch immer die Sitzung liefert". Ein
|
||||
// laufender Sender hat keine Fassungen zur Auswahl.
|
||||
mediaIndex: -1,
|
||||
partIndex: -1,
|
||||
maximumBitrate: maximumBitrate,
|
||||
isLocal: connection.local,
|
||||
sessionIdentifier: session,
|
||||
pathOverride: streamPath
|
||||
)),
|
||||
sessionIdentifier: session,
|
||||
trace: trace
|
||||
)
|
||||
).content.first
|
||||
} catch {
|
||||
trace.append(ChannelProbe(path: path, result: Self.shortDescription(of: error)))
|
||||
trace.append(ChannelProbe(path: openPath, result: Self.shortDescription(of: error)))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Letzter Versuch: die Sendung unmittelbar umrechnen lassen. Auf
|
||||
// Servern, die den Tuner selbst belegen, genügt das.
|
||||
if let metadataKey = program?.metadataKey {
|
||||
trace.append(ChannelProbe(path: metadataKey, result: "direkt versucht"))
|
||||
return LiveTuneResult(
|
||||
url: transcodeURL(for: TranscodeRequest(
|
||||
ratingKey: channel.id,
|
||||
guard let item else {
|
||||
trace.append(ChannelProbe(path: openPath, result: "leer"))
|
||||
continue
|
||||
}
|
||||
trace.append(ChannelProbe(path: openPath, result: "Sitzung offen"))
|
||||
|
||||
for candidate in Self.streamCandidates(item: item, sessionPath: sessionPath) {
|
||||
guard attempts < 8 else { break }
|
||||
attempts += 1
|
||||
|
||||
let request = TranscodeRequest(
|
||||
ratingKey: item.ratingKey,
|
||||
mediaIndex: candidate.mediaIndex,
|
||||
partIndex: candidate.mediaIndex,
|
||||
maximumBitrate: maximumBitrate,
|
||||
isLocal: connection.local,
|
||||
sessionIdentifier: session,
|
||||
pathOverride: metadataKey
|
||||
)),
|
||||
sessionIdentifier: session,
|
||||
trace: trace
|
||||
)
|
||||
pathOverride: candidate.path
|
||||
)
|
||||
guard let url = transcodeURL(for: request) else { continue }
|
||||
|
||||
let outcome = await probeStream(url: url)
|
||||
trace.append(ChannelProbe(
|
||||
path: "\(candidate.path) (mediaIndex \(candidate.mediaIndex))",
|
||||
result: outcome
|
||||
))
|
||||
|
||||
if outcome.hasPrefix("HTTP 200") {
|
||||
return LiveTuneResult(url: url, sessionIdentifier: session, trace: trace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Letzter Versuch ohne eigene Sitzung: die Sendung unmittelbar
|
||||
// umrechnen lassen. Auf Servern, die den Tuner selbst belegen, genügt
|
||||
// das — und geprüft wird es genauso wie alles andere.
|
||||
if let metadataKey = program?.metadataKey, attempts < 8 {
|
||||
let request = TranscodeRequest(
|
||||
ratingKey: channel.id,
|
||||
maximumBitrate: maximumBitrate,
|
||||
isLocal: connection.local,
|
||||
sessionIdentifier: session,
|
||||
pathOverride: metadataKey
|
||||
)
|
||||
if let url = transcodeURL(for: request) {
|
||||
let outcome = await probeStream(url: url)
|
||||
trace.append(ChannelProbe(path: metadataKey, result: outcome))
|
||||
if outcome.hasPrefix("HTTP 200") {
|
||||
return LiveTuneResult(url: url, sessionIdentifier: session, trace: trace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kein Weg hat mit 200 geantwortet. Den Transcoder anhalten, sonst
|
||||
// rechnet der Server für eine Wiedergabe weiter, die es nicht gibt.
|
||||
await stopTranscode(sessionIdentifier: session)
|
||||
return LiveTuneResult(url: nil, sessionIdentifier: session, trace: trace)
|
||||
}
|
||||
|
||||
/// Welche Adresse mit welchem Index umzurechnen ist.
|
||||
///
|
||||
/// Die Adresse der Sitzung zuerst — der Transcoder sucht sich den Teil
|
||||
/// dann selbst, und dafür steht der Index -1. Die übrigen Formen stehen
|
||||
/// dahinter, weil sich dieser Teil der Schnittstelle zwischen
|
||||
/// Serverfassungen unterscheidet und nirgends beschrieben ist.
|
||||
static func streamCandidates(
|
||||
item: PlexItem,
|
||||
sessionPath: String
|
||||
) -> [(path: String, mediaIndex: Int)] {
|
||||
var paths = [sessionPath]
|
||||
if item.key.hasPrefix("/livetv/"), !paths.contains(item.key) { paths.append(item.key) }
|
||||
if let part = item.media.first?.parts.first?.key, !paths.contains(part) { paths.append(part) }
|
||||
|
||||
return paths.flatMap { path in
|
||||
[(path: path, mediaIndex: -1), (path: path, mediaIndex: 0)]
|
||||
}
|
||||
}
|
||||
|
||||
/// Holt eine Stromadresse einmal selbst ab und sagt, was zurückkam.
|
||||
///
|
||||
/// **Ohne Plex-Kopfzeilen**, weil AVPlayer sie ebenfalls nicht schickt.
|
||||
/// Mit Kopfzeilen zu prüfen hieße, etwas anderes zu prüfen als das, was
|
||||
/// später wirklich passiert — und dann stünde der Fehler wieder erst im
|
||||
/// Player, wo niemand mehr sieht, woran es lag.
|
||||
public func probeStream(url: URL) async -> String {
|
||||
var request = URLRequest(url: url)
|
||||
request.timeoutInterval = 20
|
||||
|
||||
do {
|
||||
let (data, response) = try await transport.data(for: request)
|
||||
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
|
||||
guard status != 200 else { return "HTTP 200" }
|
||||
|
||||
// Plex legt dem Fehler eine Begründung bei. Genau die fehlte
|
||||
// bisher: der Player zeigte „HTTP 400 (unhandled)" und sonst nichts.
|
||||
let body = String(decoding: data.prefix(300), as: UTF8.self)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.replacingOccurrences(of: "\n", with: " ")
|
||||
return body.isEmpty ? "HTTP \(status)" : "HTTP \(status): \(body)"
|
||||
} catch {
|
||||
return Self.shortDescription(of: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unter welchen Schlüsseln sich eine Live-Sitzung eröffnen lässt.
|
||||
///
|
||||
/// Der Raster-Schlüssel zuerst: er bezeichnet den Sender genau so, wie
|
||||
|
||||
Reference in New Issue
Block a user