feat: show skip offer countdown and transitions

This commit is contained in:
2026-10-22 14:09:53 +02:00
parent 70672321a1
commit 53369424e6
13 changed files with 453 additions and 40 deletions
+111
View File
@@ -94,6 +94,113 @@ struct ResolvedPlaybackCheck {
}
print("PASS: default VTT selected, forward/backward seek and acknowledged cue timing")
if !input.hasPrefix("https://") { try await segmentSkips(model, clock: clock, file: file) }
if !input.hasPrefix("https://") {
model.shutdown()
try await streamQueueLifecycle(source: source)
}
}
static func streamQueueLifecycle(source: ResolvedSource) async throws {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("terebi.queue-check." + UUID().uuidString).resolvingSymlinksInPath()
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true,
attributes: [.posixPermissions: 0o700])
defer { try? FileManager.default.removeItem(at: directory) }
let youtubeURL = URL(string: "https://www.youtube.com/watch?v=AAAAAAAAAAA")!
let episodeURL = URL(string: "https://barelystarted.miruro.tv/watch/fixture-ID/episode?ep=1")!
let firstReport = directory.appendingPathComponent("youtube.jsonl")
let secondReport = directory.appendingPathComponent("miruro.jsonl")
let queue = StreamQueue(entries: [
.init(url: youtubeURL, identity: youtubeURL.absoluteString, progressFile: firstReport, label: "First"),
.init(url: episodeURL, identity: source.identity, progressFile: secondReport, label: "Second")
])
let suite = "terebi.queue-check." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
var youtubeRequests = 0
var episodeRequests = 0
var stale: CheckedContinuation<YouTubeVideo, Never>?
let video = YouTubeVideo(title: "Queue YouTube fixture",
video: .init(url: source.url, headers: source.headers), audio: nil,
duration: 0, isLive: false, webpageURL: youtubeURL, height: nil, availableHeights: [])
let model = PlayerModel(defaults: defaults, streamQueue: queue, youtubeResolver: { _, _ in
youtubeRequests += 1
if youtubeRequests == 1 {
return await withCheckedContinuation { stale = $0 }
}
return video
}, miruroResolver: { _, _ in
episodeRequests += 1
return source
})
defer { model.shutdown() }
require(model.progressInitializationError == nil, "queue report registration")
model.openStreamQueue(queue)
try await until("only selected first stream resolves", model: model) { stale != nil }
require(episodeRequests == 0 && model.queue.items.map(\.url) == [youtubeURL, episodeURL],
"stream queue is lazy and preserves supplied order")
let firstID = model.queue.items[0].id
let secondID = model.queue.items[1].id
model.selectQueueItem(secondID)
try await until("manual stream selection decodes native media", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.state.position > 0.2
}
stale!.resume(returning: video)
stale = nil
// Yield to the canceled resolver; it must not open or redirect its report.
for _ in 0..<10 { await Task.yield() }
require(model.queue.currentID == secondID && model.fileName == source.title &&
!FileManager.default.fileExists(atPath: firstReport.path), "stale extraction cannot replace selected media or report")
model.advanceQueue(-1)
try await until("previous resolves YouTube afresh", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.fileName == video.title
}
require(youtubeRequests == 2 && model.queue.currentID == firstID, "manual previous preserves queue and resolves current item")
require(model.setPlayback(playing: false), "pause first queued stream")
try await until("queue pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
require(model.seekRelative(max(0, model.duration - 0.3) - model.state.position), "seek first stream near natural end")
try await until("end seek acknowledged", model: model) {
!model.state.seek_pending && !model.isBusy && abs(model.state.position - max(0, model.duration - 0.3)) < 0.1
}
require(model.setPlayback(playing: true), "play first stream to natural end")
try await until("natural end advances and freshly resolves second stream", model: model) {
model.queue.currentID == secondID && !model.isBusy && model.state.status == PLAYER_PLAYING &&
model.fileName == source.title && episodeRequests == 2
}
require(model.setPlayback(playing: false), "pause second stream for restoration")
try await until("second paused", model: model) { model.state.status == PLAYER_PAUSED }
require(model.seekRelative(0.8 - model.state.position), "save an acknowledged second-stream bookmark")
try await until("second bookmark acknowledged", model: model) {
!model.state.seek_pending && abs(model.state.position - 0.8) < 0.1
}
model.advanceQueue(-1)
try await until("revisit first report session", model: model) {
!model.isBusy && model.fileName == video.title && model.state.status == PLAYER_PLAYING
}
model.advanceQueue(1)
try await until("second revisit resumes acknowledged bookmark", model: model) {
!model.isBusy && model.fileName == source.title && model.state.status == PLAYER_PLAYING &&
model.state.position >= 0.7
}
require(youtubeRequests == 3 && episodeRequests == 3, "every revisit resolves fresh transport metadata")
model.shutdown()
func records(_ file: URL) throws -> [[String: Any]] {
try String(contentsOf: file, encoding: .utf8).split(separator: "\n").map {
try JSONSerialization.jsonObject(with: Data($0.utf8)) as! [String: Any]
}
}
let youtubeRows = try records(firstReport)
let episodeRows = try records(secondReport)
require(youtubeRows.count >= 4 && youtubeRows.allSatisfy {
$0["version"] as? Int == 1 && $0["media_url"] as? String == youtubeURL.absoluteString
}, "YouTube report appends on revisit without another identity")
require(youtubeRows.contains { $0["event"] as? String == "ended" }, "natural end retains its acknowledged completion snapshot")
require(episodeRows.count >= 6 && episodeRows.allSatisfy {
let identity = $0["media_identity"] as? [String: Any]
return $0["version"] as? Int == 2 && identity?["media_id"] as? String == "fixture-ID" &&
identity?["episode"] as? Int == 1
}, "Miruro report is isolated through selection, advance and revisits")
print("PASS: ordered lazy mixed stream queue, stale cancellation, native end advance, fresh revisits/resume and report isolation")
}
static func segmentSkips(_ model: PlayerModel, clock: Clock, file: URL) async throws {
@@ -107,10 +214,14 @@ struct ResolvedPlaybackCheck {
}
model.supplyMetadataForCheck(metadata, episode: 1)
require(model.segmentToSkip == opening, "late metadata offers opening at acknowledged paused position")
require(model.skipOfferRemainingFraction == 1, "late entry starts with a full countdown background")
clock.now = 105
require(model.skipOfferRemainingFraction == 0.5, "paused countdown drains by elapsed offer time, not media position")
clock.now = 109.999
require(model.segmentToSkip == opening, "opening remains available before ten seconds")
clock.now = 110
require(model.segmentToSkip == nil && !model.skipSegment(), "offer expires at ten seconds while paused")
require(model.skipOfferRemainingFraction == 0, "expired offer has no remaining background")
clock.now = 111
try await Task.sleep(for: .milliseconds(100))
require(model.segmentToSkip == nil, "polling does not reoffer ignored opening")