Bei „Auto\" bestellen wir eine Liste von Stufen; die Schnipsel stehen eine Ebene darunter. Der Speicher erkannte das zwar, stieg dann aber aus und reichte die Liste mit relativen Adressen durch — die zeigten danach auf den lokalen Server, wo es sie nicht gibt. Es wurde nichts vorgeholt, und beinahe waere auch nichts mehr gelaufen. Jetzt wird auch die Stufenliste umgeschrieben. Holt der Player eine Stufe, kommt er wieder bei uns an, und dort beginnt das Vorauslaufen — bei der Stufe, fuer die er sich entschieden hat. Dazu eine Zeile im Qualitaetsblatt, die sagt, ob der Vorrat laeuft und wie weit er voraus ist. Von aussen ist das nicht zu sehen: laeuft er nicht, spielt es trotzdem, nur ohne Vorsprung — und genau so sieht auch ein Vorrat aus, der laeuft und nichts bekommt. Ohne diese Zeile sind die beiden Faelle nicht zu unterscheiden, und ich haette wieder geraten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
146 lines
5.6 KiB
Swift
146 lines
5.6 KiB
Swift
import Testing
|
|
import Foundation
|
|
@testable import FlixyPlayback
|
|
|
|
@Suite("Abspielliste lesen und umschreiben")
|
|
struct MediaPlaylistTests {
|
|
|
|
private let medien = """
|
|
#EXTM3U
|
|
#EXT-X-VERSION:7
|
|
#EXT-X-TARGETDURATION:6
|
|
#EXT-X-MEDIA-SEQUENCE:0
|
|
#EXT-X-MAP:URI="hls1/main/-1.mp4"
|
|
#EXTINF:6.000000,
|
|
hls1/main/0.mp4
|
|
#EXTINF:6.000000,
|
|
hls1/main/1.mp4
|
|
#EXTINF:5.400000,
|
|
hls1/main/2.mp4
|
|
#EXT-X-ENDLIST
|
|
"""
|
|
|
|
private let stufen = """
|
|
#EXTM3U
|
|
#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1920x1080
|
|
main.m3u8?level=0
|
|
#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1280x720
|
|
main.m3u8?level=1
|
|
"""
|
|
|
|
@Test("Die Schnipsel kommen in der Reihenfolge der Liste")
|
|
func readsSegmentsInOrder() {
|
|
let liste = MediaPlaylist(medien)
|
|
#expect(liste.segments == ["hls1/main/0.mp4", "hls1/main/1.mp4", "hls1/main/2.mp4"])
|
|
#expect(!liste.isMaster)
|
|
}
|
|
|
|
/// Die beiden Listenarten sehen sich ähnlich und meinen Verschiedenes.
|
|
/// Wer sie verwechselt, lädt Abspiellisten vor statt Bild.
|
|
@Test("Eine Liste von Stufen ist keine Liste von Schnipseln")
|
|
func tellsMasterFromMedia() {
|
|
#expect(MediaPlaylist(stufen).isMaster)
|
|
#expect(MediaPlaylist(stufen).segments.count == 2)
|
|
#expect(!MediaPlaylist(medien).isMaster)
|
|
}
|
|
|
|
/// **Die teuerste Stelle.** Eine Liste, die stellenweise am Speicher
|
|
/// vorbeizeigt, ergibt einen Film, der nur manchmal hakt — und so etwas
|
|
/// 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)" }
|
|
|
|
#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"))
|
|
// Anweisungen bleiben, wie sie waren — auch die mit einer Adresse
|
|
// **darin**, die wir nicht verstehen.
|
|
#expect(neu.contains("#EXT-X-TARGETDURATION:6"))
|
|
#expect(neu.contains("#EXT-X-MAP:URI=\"hls1/main/-1.mp4\""))
|
|
#expect(neu.contains("#EXT-X-ENDLIST"))
|
|
#expect(neu.hasPrefix("#EXTM3U"))
|
|
}
|
|
|
|
@Test("Die Zeilenzahl bleibt gleich")
|
|
func keepsStructure() {
|
|
let neu = MediaPlaylist(medien).rewritingReferences { $0 }
|
|
#expect(neu.components(separatedBy: "\n").count == medien.components(separatedBy: "\n").count)
|
|
}
|
|
|
|
@Test("Eine vollständige Liste ist als solche zu erkennen")
|
|
func knowsWhenComplete() {
|
|
#expect(MediaPlaylist(medien).isComplete)
|
|
// Ohne Schlussmarke: ein laufender Sender, die Liste wächst noch.
|
|
#expect(!MediaPlaylist(medien.replacingOccurrences(of: "#EXT-X-ENDLIST", with: "")).isComplete)
|
|
}
|
|
|
|
@Test("Die Schnipseldauer wird gelesen")
|
|
func readsTargetDuration() {
|
|
#expect(MediaPlaylist(medien).targetDuration == .seconds(6))
|
|
#expect(MediaPlaylist(stufen).targetDuration == nil)
|
|
}
|
|
|
|
@Test("Eine leere Eingabe ergibt eine leere Liste, keinen Absturz")
|
|
func emptyIsEmpty() {
|
|
#expect(MediaPlaylist("").segments.isEmpty)
|
|
#expect(MediaPlaylist("").rewritingReferences { $0 } == "")
|
|
}
|
|
}
|
|
|
|
@Suite("Adressen für den lokalen Speicher")
|
|
struct LocalStreamServerPathTests {
|
|
/// Der Fehler, an dem der erste Anlauf gescheitert ist: eine Liste von
|
|
/// Stufen wurde durchgereicht, ihre **relativen** Adressen zeigten danach
|
|
/// auf den lokalen Server — und dort gibt es sie nicht.
|
|
@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"
|
|
)
|
|
let teile = URLComponents(string: pfad)
|
|
#expect(teile?.path == "/level.m3u8")
|
|
#expect(
|
|
teile?.queryItems?.first { $0.name == "u" }?.value
|
|
== "http://server:8096/videos/x/main.m3u8?level=1&api_key=t"
|
|
)
|
|
}
|
|
|
|
/// Die Endung muss stehen bleiben: AVPlayer entscheidet am Namen, ob er
|
|
/// eine Abspielliste oder Bild erwartet.
|
|
@Test("Die Endung bleibt eine Abspielliste")
|
|
func levelPathKeepsExtension() {
|
|
#expect(LocalStreamServer.localPlaylistPath(for: "http://s/a.m3u8").hasPrefix("/level.m3u8"))
|
|
}
|
|
|
|
/// Schrägstriche und Punkte im ursprünglichen Pfad dürfen nichts
|
|
/// anrichten — deshalb wandert er in die Abfrage, nicht in den Pfad.
|
|
@Test("Der Schnipselpfad überlebt Schrägstriche")
|
|
func segmentPathSurvivesSlashes() {
|
|
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")
|
|
}
|
|
|
|
/// Eine Stufenliste, durch den Umschreiber gedreht, muss auf den lokalen
|
|
/// Server zeigen — und zwar jede Zeile.
|
|
@Test("Jede Stufe zeigt danach auf uns")
|
|
func everyLevelIsRewritten() {
|
|
let stufen = """
|
|
#EXTM3U
|
|
#EXT-X-STREAM-INF:BANDWIDTH=8000000
|
|
main.m3u8?level=0
|
|
#EXT-X-STREAM-INF:BANDWIDTH=3000000
|
|
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 absolut = URL(string: eintrag, relativeTo: basis)?.absoluteString ?? eintrag
|
|
return LocalStreamServer.localPlaylistPath(for: absolut)
|
|
}
|
|
|
|
#expect(neu.components(separatedBy: "/level.m3u8").count == 3)
|
|
#expect(!neu.contains("\nmain.m3u8"))
|
|
}
|
|
}
|