Der Vorrat laeuft jetzt voraus, nicht der Player
AVPlayer entscheidet selbst, wie weit er puffert, und bei einer Abspielliste laesst Apple uns nicht dazwischen: auf eine Anfrage nach einem Schnipsel nimmt er von uns nur eine Weiterleitung an, keine Daten. Der Denkfehler war, seinen Puffer vergroessern zu wollen. Wir muessen aber gar nicht dazwischen — wir koennen die Gegenstelle sein. Ein winziger HTTP-Server auf 127.0.0.1 liefert dem Player die Abspielliste, umgeschrieben auf sich selbst, und holt die Schnipsel zuegiger von Jellyfin, als abgespielt wird. Jedes Segment, das der Player verlangt, liegt dann schon lokal. Faellt die Verbindung im Tunnel aus, merkt er es nicht. Das ist der Unterschied zwischen Puffer und Vorrat: den Puffer bestimmt Apple, den Vorrat bestimmen wir. Streng der Reihe nach, und mit begrenztem Vorsprung. Der Server erzeugt die Schnipsel gerade erst; wer Nummer 40 verlangt, waehrend er bei 12 ist, sieht fuer Jellyfin aus wie jemand, der gesprungen ist — und dann beginnt das Umrechnen von vorn, an der falschen Stelle. Der Lauscher haengt ausdruecklich an 127.0.0.1, nicht an allen Schnittstellen. Ein Server, der den Film ins Netz stellt, waere das Letzte, was eine Mediathek-App tun sollte. Kommt er nicht hoch, spielt es trotzdem — ueber die urspruengliche Adresse, ohne Vorrat. Ein fehlender Vorrat ist ein Verlust an Bequemlichkeit; keine Wiedergabe ist ein Verlust an allem. Das Lesen und Umschreiben der Abspielliste ist ein eigener Typ ohne Netz, mit sieben Tests. Eine falsch umgeschriebene Liste ergibt entweder ein schwarzes Bild oder — schlimmer — eine, die stellenweise am Speicher vorbeigeht und nur manchmal hakt. So etwas findet man im laufenden Film nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
46550fc30b
commit
134dc82927
@@ -0,0 +1,88 @@
|
||||
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 } == "")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user