Untertitel brachten den Vorratsspeicher auf 404
Untertitel stehen in der Abspielliste als eigene Spur unter EXT-X-MEDIA — eine Anweisung mit einer Adresse darin. Umgeschrieben habe ich bisher nur EXT-X-MAP, also blieb diese relativ stehen und zeigte danach auf den lokalen Server, wo es sie nicht gibt. Zu sehen war der Fehler erst, als jemand Untertitel einschaltete: HTTP 404, mitten im Film. Jetzt sagt der Umschreiber, **was** fuer eine Adresse er gerade in der Hand hat: eine Qualitaetsstufe, eine Nebenspur, ein Kopfstueck oder ein Schnipsel. Das ist noetig, weil sie Verschiedenes wollen — hinter einer Stufe liegt Bild und die soll vorgeladen werden, hinter einer Untertitel- spur nicht. Sonst lueden wir Untertitel statt Bild: der Film bliebe stehen, waehrend die Anzeige reichlich Vorrat meldet. Dazu wandern alle Adressen im lokalen Server auf die **vollstaendige** Form. Eine relative gilt je nach Liste gegen eine andere Grundadresse — die Untertitelspur liegt woanders als das Bild —, und wer das erst beim Abholen aufloest, loest es irgendwann gegen die falsche auf. Das waere der naechste Fehler derselben Art gewesen. Drei Tests halten die Unterscheidung fest, darunter die Liste mit Untertitelspur, die den 404 ausgeloest hat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
88ba1a540c
commit
cff0c06745
@@ -39,17 +39,33 @@ public actor LocalStreamServer {
|
||||
/// Vollständig, nicht relativ: eine Stufe liegt beim Server unter einem
|
||||
/// ganz anderen Pfad als bei uns, und relativ aufgelöst zeigte sie ins
|
||||
/// Leere.
|
||||
public nonisolated static func localPlaylistPath(for absolute: String) -> String {
|
||||
/// - Parameter prefetch: ob dahinter Bild liegt. Bei einer Nebenspur —
|
||||
/// Untertitel oder zusätzlicher Ton — steht es auf `false`, sonst lüde
|
||||
/// der Vorrat Untertitel statt Bild, und der Film bliebe stehen,
|
||||
/// während die Anzeige reichlich Vorrat meldet.
|
||||
public nonisolated static func localPlaylistPath(
|
||||
for absolute: String,
|
||||
prefetch: Bool
|
||||
) -> String {
|
||||
var teile = URLComponents()
|
||||
teile.path = levelPrefix
|
||||
teile.queryItems = [URLQueryItem(name: "u", value: absolute)]
|
||||
teile.queryItems = [
|
||||
URLQueryItem(name: "u", value: absolute),
|
||||
URLQueryItem(name: "v", value: prefetch ? "1" : "0"),
|
||||
]
|
||||
return teile.string ?? levelPrefix
|
||||
}
|
||||
|
||||
public nonisolated static func localPath(for original: String) -> String {
|
||||
/// Wie ein Schnipsel beim Server heisst.
|
||||
///
|
||||
/// Die **vollständige** Adresse wandert in die Abfrage. Eine relative
|
||||
/// gälte je nach Liste gegen eine andere Grundadresse — die
|
||||
/// Untertitelspur liegt woanders als das Bild —, und wer das erst beim
|
||||
/// Abholen auflöst, löst es irgendwann gegen die falsche auf.
|
||||
public nonisolated static func localPath(for absolute: String) -> String {
|
||||
var teile = URLComponents()
|
||||
teile.path = segmentPrefix
|
||||
teile.queryItems = [URLQueryItem(name: "p", value: original)]
|
||||
teile.queryItems = [URLQueryItem(name: "u", value: absolute)]
|
||||
return teile.string ?? segmentPrefix
|
||||
}
|
||||
|
||||
@@ -154,7 +170,10 @@ public actor LocalStreamServer {
|
||||
if pfadTeil.hasPrefix(Self.levelPrefix) {
|
||||
guard let teile = URLComponents(string: pfadTeil),
|
||||
let adresse = teile.queryItems?.first(where: { $0.name == "u" })?.value,
|
||||
let inhalt = await cache.playlist(at: adresse)
|
||||
let inhalt = await cache.playlist(
|
||||
at: adresse,
|
||||
prefetch: teile.queryItems?.first { $0.name == "v" }?.value != "0"
|
||||
)
|
||||
else { return finish(verbindung, status: 404, body: Data()) }
|
||||
|
||||
return finish(
|
||||
@@ -173,10 +192,10 @@ public actor LocalStreamServer {
|
||||
|
||||
guard pfadTeil.hasPrefix(Self.segmentPrefix),
|
||||
let teile = URLComponents(string: pfadTeil),
|
||||
let original = teile.queryItems?.first(where: { $0.name == "p" })?.value
|
||||
let adresse = teile.queryItems?.first(where: { $0.name == "u" })?.value
|
||||
else { return finish(verbindung, status: 404, body: Data()) }
|
||||
|
||||
guard let daten = await cache.data(for: original) else {
|
||||
guard let daten = await cache.data(for: adresse) else {
|
||||
return finish(verbindung, status: 504, body: Data())
|
||||
}
|
||||
finish(verbindung, status: 200, body: daten, type: "video/mp4")
|
||||
|
||||
@@ -62,52 +62,71 @@ public struct MediaPlaylist: Sendable, Equatable {
|
||||
self.isMaster = master
|
||||
}
|
||||
|
||||
/// Was für eine Adresse an einer Stelle steht.
|
||||
///
|
||||
/// **Warum das jemanden interessiert.** Eine Abspielliste enthält
|
||||
/// viererlei Adressen, und sie wollen Verschiedenes: eine Qualitätsstufe
|
||||
/// führt zu weiteren Schnipseln und soll vorgeladen werden, eine
|
||||
/// Untertitelspur führt zu einer eigenen Liste und soll es **nicht** —
|
||||
/// sonst lädt der Vorrat Untertitel statt Bild.
|
||||
public enum Reference: Sendable, Equatable {
|
||||
/// Eine Qualitätsstufe — dahinter liegt das Bild.
|
||||
case level
|
||||
/// Eine Nebenspur: Untertitel oder zusätzlicher Ton.
|
||||
case rendition
|
||||
/// Das Kopfstück einer fragmentierten MP4.
|
||||
case map
|
||||
/// Ein Schnipsel.
|
||||
case segment
|
||||
}
|
||||
|
||||
/// Schreibt jede Adresse um, ohne sonst etwas anzufassen.
|
||||
///
|
||||
/// Die Reihenfolge bleibt, die Anweisungen bleiben, die Leerzeilen
|
||||
/// bleiben. Nur wo eine Adresse stand, steht danach eine andere.
|
||||
public func rewritingReferences(_ transform: (String) -> String) -> String {
|
||||
///
|
||||
/// **Auch die Adressen in Anweisungen.** Zwei Anweisungen tragen eine:
|
||||
/// `EXT-X-MAP` das Kopfstück und `EXT-X-MEDIA` die Nebenspuren. Beide
|
||||
/// stehen zu lassen ergibt eine Liste, die halb auf uns und halb ins
|
||||
/// Leere zeigt — und das endet mit „404", sobald jemand Untertitel
|
||||
/// einschaltet.
|
||||
public func rewritingReferences(_ transform: (String, Reference) -> String) -> String {
|
||||
var ergebnis: [String] = []
|
||||
ergebnis.reserveCapacity(lines.count)
|
||||
var nächsteIstStufe = false
|
||||
|
||||
for zeile in lines {
|
||||
let getrimmt = zeile.trimmingCharacters(in: .whitespaces)
|
||||
if getrimmt.isEmpty {
|
||||
|
||||
if getrimmt.hasPrefix("#EXT-X-STREAM-INF") {
|
||||
nächsteIstStufe = true
|
||||
ergebnis.append(zeile)
|
||||
} else if getrimmt.hasPrefix("#EXT-X-MEDIA:") {
|
||||
ergebnis.append(Self.rewritingURI(in: zeile) { transform($0, .rendition) })
|
||||
} else if getrimmt.hasPrefix("#EXT-X-MAP:") {
|
||||
// **Die eine Anweisung, in der eine Adresse steckt.**
|
||||
//
|
||||
// Sie zeigt auf das Kopfstück einer fragmentierten MP4 — ohne
|
||||
// das ist kein einziger Schnipsel zu dekodieren. Bliebe sie
|
||||
// stehen, zeigte sie relativ auf den lokalen Server, und dort
|
||||
// gibt es sie nicht: schwarzes Bild, ohne dass an den
|
||||
// Schnipseln irgendetwas falsch wäre.
|
||||
ergebnis.append(Self.rewritingMapURI(zeile, transform))
|
||||
} else if getrimmt.hasPrefix("#") {
|
||||
ergebnis.append(Self.rewritingURI(in: zeile) { transform($0, .map) })
|
||||
} else if getrimmt.isEmpty || getrimmt.hasPrefix("#") {
|
||||
ergebnis.append(zeile)
|
||||
} else {
|
||||
ergebnis.append(transform(getrimmt))
|
||||
ergebnis.append(transform(getrimmt, nächsteIstStufe ? .level : .segment))
|
||||
nächsteIstStufe = false
|
||||
}
|
||||
}
|
||||
return ergebnis.joined(separator: "\n")
|
||||
}
|
||||
|
||||
/// Tauscht die Adresse in `#EXT-X-MAP:URI="…"` aus, sonst nichts.
|
||||
private static func rewritingMapURI(
|
||||
_ zeile: String,
|
||||
/// Tauscht die Adresse in `URI="…"` aus, sonst nichts.
|
||||
private static func rewritingURI(
|
||||
in zeile: String,
|
||||
_ transform: (String) -> String
|
||||
) -> String {
|
||||
let marke = "URI=\""
|
||||
guard let start = zeile.range(of: marke),
|
||||
let ende = zeile[start.upperBound...].firstIndex(of: "\"")
|
||||
else { return zeile }
|
||||
let ende2 = ende..<zeile.index(after: ende)
|
||||
|
||||
let adresse = String(zeile[start.upperBound..<ende2.lowerBound])
|
||||
return zeile.replacingCharacters(
|
||||
in: start.upperBound..<ende2.lowerBound,
|
||||
with: transform(adresse)
|
||||
)
|
||||
let adresse = String(zeile[start.upperBound..<ende])
|
||||
return zeile.replacingCharacters(in: start.upperBound..<ende, with: transform(adresse))
|
||||
}
|
||||
|
||||
/// Ob die Liste vollständig ist — dann kommen keine Schnipsel mehr dazu.
|
||||
|
||||
@@ -77,49 +77,55 @@ public actor StreamCache {
|
||||
|
||||
/// Nimmt die Abspielliste entgegen und beginnt vorauszulaufen.
|
||||
///
|
||||
/// Gibt die umgeschriebene Liste zurück — die, die der Player bekommt.
|
||||
///
|
||||
/// **Zwei Ebenen, und beide müssen durch uns.** Bei „Auto" bestellen wir
|
||||
/// eine Liste von **Stufen**; die Schnipsel stehen eine Ebene tiefer. Wer
|
||||
/// hier aussteigt, reicht relative Adressen durch, die danach auf den
|
||||
/// lokalen Server zeigen — und dort gibt es sie nicht. Genau daran ist
|
||||
/// der erste Anlauf gescheitert: es wurde nichts vorgeholt, und beinahe
|
||||
/// wäre auch nichts mehr gelaufen.
|
||||
///
|
||||
/// Also wird auch die Stufenliste umgeschrieben. Holt der Player dann
|
||||
/// eine Stufe, kommt er wieder bei uns an, und **dort** beginnt das
|
||||
/// Vorauslaufen — bei der Stufe, für die er sich entschieden hat.
|
||||
/// **Alles wird auf eine vollständige Adresse umgeschrieben.** Relative
|
||||
/// Angaben gelten je nach Liste gegen eine andere Grundadresse — die
|
||||
/// Untertitelspur liegt woanders als das Bild. Wer das erst beim Abholen
|
||||
/// auflöst, löst es irgendwann gegen die falsche auf. Hier steht die
|
||||
/// Grundadresse noch fest.
|
||||
public func start(upstream url: URL, headers: [String: String] = [:]) async throws -> String {
|
||||
self.headers = headers
|
||||
let (liste, _) = try await load(url)
|
||||
|
||||
guard !liste.isMaster else {
|
||||
return liste.rewritingReferences { eintrag in
|
||||
let absolut = URL(string: eintrag, relativeTo: url)?.absoluteString ?? eintrag
|
||||
return LocalStreamServer.localPlaylistPath(for: absolut)
|
||||
}
|
||||
}
|
||||
return adopt(liste, from: url)
|
||||
let liste = try await load(url)
|
||||
return rewrite(liste, base: url, adopting: !liste.isMaster)
|
||||
}
|
||||
|
||||
/// Eine Stufe, die der Player angefordert hat.
|
||||
/// Eine Liste, die der Player eine Ebene tiefer angefordert hat.
|
||||
///
|
||||
/// Ab hier stehen die Schnipsel fest, und der Vorrat beginnt zu laufen.
|
||||
public func playlist(at address: String) async -> String? {
|
||||
guard let url = URL(string: address), let (liste, _) = try? await load(url) else { return nil }
|
||||
guard !liste.isMaster else { return liste.rewritingReferences { $0 } }
|
||||
return adopt(liste, from: url)
|
||||
/// `prefetch` sagt, ob dahinter Bild liegt. Bei einer Nebenspur — also
|
||||
/// Untertiteln — steht es auf `false`: sonst lüde der Vorrat Untertitel
|
||||
/// statt Bild, und der Film bliebe stehen, während die Kachel meldet, es
|
||||
/// sei reichlich geladen.
|
||||
public func playlist(at address: String, prefetch: Bool) async -> String? {
|
||||
guard let url = URL(string: address), let liste = try? await load(url) else { return nil }
|
||||
return rewrite(liste, base: url, adopting: prefetch && !liste.isMaster)
|
||||
}
|
||||
|
||||
private func rewrite(_ liste: MediaPlaylist, base: URL, adopting: Bool) -> String {
|
||||
if adopting { adopt(liste, base: base) }
|
||||
|
||||
return liste.rewritingReferences { adresse, art in
|
||||
let absolut = URL(string: adresse, relativeTo: base)?.absoluteString ?? adresse
|
||||
switch art {
|
||||
case .level:
|
||||
return LocalStreamServer.localPlaylistPath(for: absolut, prefetch: true)
|
||||
case .rendition:
|
||||
return LocalStreamServer.localPlaylistPath(for: absolut, prefetch: false)
|
||||
case .map, .segment:
|
||||
return LocalStreamServer.localPath(for: absolut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Übernimmt eine Liste von Schnipseln als die, der wir vorauslaufen.
|
||||
private func adopt(_ liste: MediaPlaylist, from url: URL) -> String {
|
||||
private func adopt(_ liste: MediaPlaylist, base: URL) {
|
||||
// **Nur bei einem echten Wechsel von vorn.** Der Player holt dieselbe
|
||||
// Liste immer wieder — nach einem Sprung, beim Stufenwechsel, von
|
||||
// sich aus. Jedes Mal zurückzusetzen hiesse, den Vorrat jedes Mal zu
|
||||
// vergessen und von Schnipsel null neu zu zählen.
|
||||
let neueQuelle = upstream != url
|
||||
upstream = url
|
||||
segments = liste.segments
|
||||
let neueQuelle = upstream != base
|
||||
upstream = base
|
||||
segments = liste.segments.map {
|
||||
URL(string: $0, relativeTo: base)?.absoluteString ?? $0
|
||||
}
|
||||
if let dauer = liste.targetDuration { targetDuration = dauer }
|
||||
|
||||
if neueQuelle {
|
||||
@@ -127,29 +133,26 @@ public actor StreamCache {
|
||||
fetched = 0
|
||||
}
|
||||
beginPrefetching()
|
||||
return liste.rewritingReferences { LocalStreamServer.localPath(for: $0) }
|
||||
}
|
||||
|
||||
private func load(_ url: URL) async throws -> (MediaPlaylist, Data) {
|
||||
private func load(_ url: URL) async throws -> MediaPlaylist {
|
||||
var request = URLRequest(url: url)
|
||||
for (name, wert) in headers { request.setValue(wert, forHTTPHeaderField: name) }
|
||||
let (daten, _) = try await session.data(for: request)
|
||||
return (MediaPlaylist(String(decoding: daten, as: UTF8.self)), daten)
|
||||
return MediaPlaylist(String(decoding: daten, as: UTF8.self))
|
||||
}
|
||||
|
||||
// MARK: - Ausliefern
|
||||
|
||||
/// Die Daten zu einem Pfad — aus dem Vorrat, sonst frisch geholt.
|
||||
public func data(for path: String) async -> Data? {
|
||||
if let index = segments.firstIndex(of: path) {
|
||||
// Woran der Player gerade ist. Daran hängt, wie weit wir
|
||||
// vorauslaufen — und was wir wegwerfen dürfen.
|
||||
/// Die Daten zu einer Adresse — aus dem Vorrat, sonst frisch geholt.
|
||||
public func data(for address: String) async -> Data? {
|
||||
if let index = segments.firstIndex(of: address) {
|
||||
lastRequested = max(lastRequested, index)
|
||||
trimBehind()
|
||||
}
|
||||
|
||||
if let ort = stored[path], let daten = try? Data(contentsOf: ort) { return daten }
|
||||
return await fetch(path)
|
||||
if let ort = stored[address], let daten = try? Data(contentsOf: ort) { return daten }
|
||||
return await fetch(address)
|
||||
}
|
||||
|
||||
// MARK: - Vorauslaufen
|
||||
@@ -201,14 +204,13 @@ public actor StreamCache {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func fetch(_ path: String) async -> Data? {
|
||||
guard let upstream, let ziel = URL(string: path, relativeTo: upstream)?.absoluteURL
|
||||
else { return nil }
|
||||
private func fetch(_ address: String) async -> Data? {
|
||||
guard let ziel = URL(string: address) else { return nil }
|
||||
|
||||
do {
|
||||
let (daten, antwort) = try await session.data(from: ziel)
|
||||
guard (antwort as? HTTPURLResponse)?.statusCode ?? 200 < 400 else { return nil }
|
||||
store(daten, at: path)
|
||||
store(daten, at: address)
|
||||
return daten
|
||||
} catch {
|
||||
return nil
|
||||
|
||||
@@ -49,7 +49,7 @@ struct MediaPlaylistTests {
|
||||
/// findet man im laufenden Bild nicht.
|
||||
@Test("Umgeschrieben werden Adressen, sonst nichts")
|
||||
func rewritesOnlyReferences() {
|
||||
let neu = MediaPlaylist(medien).rewritingReferences { "http://127.0.0.1:1234/s/\($0)" }
|
||||
let neu = MediaPlaylist(medien).rewritingReferences { adresse, _ in "http://127.0.0.1:1234/s/\(adresse)" }
|
||||
|
||||
#expect(neu.contains("http://127.0.0.1:1234/s/hls1/main/0.mp4"))
|
||||
#expect(neu.contains("http://127.0.0.1:1234/s/hls1/main/2.mp4"))
|
||||
@@ -68,20 +68,20 @@ struct MediaPlaylistTests {
|
||||
/// irgendetwas falsch wäre.
|
||||
@Test("Das Kopfstück wandert mit")
|
||||
func rewritesInitSegment() {
|
||||
let neu = MediaPlaylist(medien).rewritingReferences { "/s/\($0)" }
|
||||
let neu = MediaPlaylist(medien).rewritingReferences { adresse, _ in "/s/\(adresse)" }
|
||||
#expect(neu.contains("#EXT-X-MAP:URI=\"/s/hls1/main/-1.mp4\""))
|
||||
}
|
||||
|
||||
@Test("Eine Anweisung ohne Adresse bleibt unberührt")
|
||||
func leavesOtherTagsAlone() {
|
||||
let neu = MediaPlaylist(medien).rewritingReferences { "/s/\($0)" }
|
||||
let neu = MediaPlaylist(medien).rewritingReferences { adresse, _ in "/s/\(adresse)" }
|
||||
#expect(neu.contains("#EXT-X-VERSION:7"))
|
||||
#expect(neu.contains("#EXTINF:6.000000,"))
|
||||
}
|
||||
|
||||
@Test("Die Zeilenzahl bleibt gleich")
|
||||
func keepsStructure() {
|
||||
let neu = MediaPlaylist(medien).rewritingReferences { $0 }
|
||||
let neu = MediaPlaylist(medien).rewritingReferences { adresse, _ in adresse }
|
||||
#expect(neu.components(separatedBy: "\n").count == medien.components(separatedBy: "\n").count)
|
||||
}
|
||||
|
||||
@@ -101,7 +101,63 @@ struct MediaPlaylistTests {
|
||||
@Test("Eine leere Eingabe ergibt eine leere Liste, keinen Absturz")
|
||||
func emptyIsEmpty() {
|
||||
#expect(MediaPlaylist("").segments.isEmpty)
|
||||
#expect(MediaPlaylist("").rewritingReferences { $0 } == "")
|
||||
#expect(MediaPlaylist("").rewritingReferences { adresse, _ in adresse } == "")
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Nebenspuren in der Abspielliste")
|
||||
struct PlaylistRenditionTests {
|
||||
/// **Der Fehler, der mit „404" endete.** Untertitel stehen in einer
|
||||
/// eigenen Anweisung mit einer Adresse darin. Blieb sie stehen, zeigte
|
||||
/// sie relativ auf den lokalen Server — und dort gibt es sie nicht. Zu
|
||||
/// sehen war das erst, als jemand Untertitel einschaltete.
|
||||
private let mitUntertiteln = """
|
||||
#EXTM3U
|
||||
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Deutsch",URI="subtitles/0.m3u8"
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=8000000,SUBTITLES="subs"
|
||||
main.m3u8?level=0
|
||||
"""
|
||||
|
||||
@Test("Die Untertitelspur wird mit umgeschrieben")
|
||||
func rewritesSubtitleRendition() {
|
||||
let neu = MediaPlaylist(mitUntertiteln).rewritingReferences { adresse, _ in
|
||||
"/um/\(adresse)"
|
||||
}
|
||||
#expect(neu.contains("URI=\"/um/subtitles/0.m3u8\""))
|
||||
#expect(!neu.contains("URI=\"subtitles/0.m3u8\""))
|
||||
}
|
||||
|
||||
/// **Und sie darf nicht vorgeladen werden.** Sonst lüde der Vorrat
|
||||
/// Untertitel statt Bild: der Film bliebe stehen, während die Anzeige
|
||||
/// reichlich Vorrat meldet.
|
||||
@Test("Stufe und Nebenspur werden unterschieden")
|
||||
func tellsLevelFromRendition() {
|
||||
var arten: [String: MediaPlaylist.Reference] = [:]
|
||||
_ = MediaPlaylist(mitUntertiteln).rewritingReferences { adresse, art in
|
||||
arten[adresse] = art
|
||||
return adresse
|
||||
}
|
||||
|
||||
#expect(arten["main.m3u8?level=0"] == .level)
|
||||
#expect(arten["subtitles/0.m3u8"] == .rendition)
|
||||
}
|
||||
|
||||
@Test("Das Kopfstück gilt als Schnipsel, nicht als Liste")
|
||||
func mapIsASegment() {
|
||||
let liste = """
|
||||
#EXTM3U
|
||||
#EXT-X-MAP:URI="hls1/main/-1.mp4"
|
||||
#EXTINF:6.0,
|
||||
hls1/main/0.mp4
|
||||
"""
|
||||
var arten: [String: MediaPlaylist.Reference] = [:]
|
||||
_ = MediaPlaylist(liste).rewritingReferences { adresse, art in
|
||||
arten[adresse] = art
|
||||
return adresse
|
||||
}
|
||||
|
||||
#expect(arten["hls1/main/-1.mp4"] == .map)
|
||||
#expect(arten["hls1/main/0.mp4"] == .segment)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +169,8 @@ struct LocalStreamServerPathTests {
|
||||
@Test("Eine Stufe wird vollständig adressiert, nicht relativ")
|
||||
func levelPathIsAbsolute() {
|
||||
let pfad = LocalStreamServer.localPlaylistPath(
|
||||
for: "http://server:8096/videos/x/main.m3u8?level=1&api_key=t"
|
||||
for: "http://server:8096/videos/x/main.m3u8?level=1&api_key=t",
|
||||
prefetch: true
|
||||
)
|
||||
let teile = URLComponents(string: pfad)
|
||||
#expect(teile?.path == "/level.m3u8")
|
||||
@@ -127,7 +184,7 @@ struct LocalStreamServerPathTests {
|
||||
/// eine Abspielliste oder Bild erwartet.
|
||||
@Test("Die Endung bleibt eine Abspielliste")
|
||||
func levelPathKeepsExtension() {
|
||||
#expect(LocalStreamServer.localPlaylistPath(for: "http://s/a.m3u8").hasPrefix("/level.m3u8"))
|
||||
#expect(LocalStreamServer.localPlaylistPath(for: "http://s/a.m3u8", prefetch: true).hasPrefix("/level.m3u8"))
|
||||
}
|
||||
|
||||
/// Schrägstriche und Punkte im ursprünglichen Pfad dürfen nichts
|
||||
@@ -137,7 +194,7 @@ struct LocalStreamServerPathTests {
|
||||
let pfad = LocalStreamServer.localPath(for: "hls1/main/12.mp4?runtimeTicks=0")
|
||||
let teile = URLComponents(string: pfad)
|
||||
#expect(teile?.path == "/s/")
|
||||
#expect(teile?.queryItems?.first { $0.name == "p" }?.value == "hls1/main/12.mp4?runtimeTicks=0")
|
||||
#expect(teile?.queryItems?.first { $0.name == "u" }?.value == "hls1/main/12.mp4?runtimeTicks=0")
|
||||
}
|
||||
|
||||
/// Eine Stufenliste, durch den Umschreiber gedreht, muss auf den lokalen
|
||||
@@ -152,9 +209,9 @@ struct LocalStreamServerPathTests {
|
||||
main.m3u8?level=1
|
||||
"""
|
||||
let basis = URL(string: "http://server:8096/videos/x/master.m3u8?api_key=t")!
|
||||
let neu = MediaPlaylist(stufen).rewritingReferences { eintrag in
|
||||
let neu = MediaPlaylist(stufen).rewritingReferences { eintrag, _ in
|
||||
let absolut = URL(string: eintrag, relativeTo: basis)?.absoluteString ?? eintrag
|
||||
return LocalStreamServer.localPlaylistPath(for: absolut)
|
||||
return LocalStreamServer.localPlaylistPath(for: absolut, prefetch: true)
|
||||
}
|
||||
|
||||
#expect(neu.components(separatedBy: "/level.m3u8").count == 3)
|
||||
|
||||
Reference in New Issue
Block a user