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")) } }