import AppKit @main @MainActor struct ResolvedPlaybackCheck { final class Clock { var now = 100.0 } static func require(_ condition: @autoclosure () -> Bool, _ message: String) { if !condition() { fatalError("FAIL: " + message) } } static func until(_ message: String, model: PlayerModel, _ predicate: () -> Bool) async throws { let deadline = ContinuousClock.now.advanced(by: .seconds(45)) while !predicate(), ContinuousClock.now < deadline { if model.state.status == PLAYER_ERROR || model.commandError != nil { fatalError("FAIL: \(message): " + (model.errorText ?? "Playback failed")) } try await Task.sleep(for: .milliseconds(10)) } require(predicate(), message) } static func main() async { do { try await run() } catch { fatalError("FAIL: " + error.localizedDescription) } } static func run() async throws { PlayerModel.metadataLifecycleCheck() if CommandLine.arguments[1] == "--metadata-lifecycle" { print("PASS: metadata lifecycle replacement, A-B-A stale results, and actual-duration changes") return } _ = NSApplication.shared NSApp.setActivationPolicy(.prohibited) let input = CommandLine.arguments[1] let file = URL(fileURLWithPath: input) let source: ResolvedSource if input.hasPrefix("https://") { guard case .miruro(let mediaID, let episode) = try StreamURL.parse(input) else { fatalError("FAIL: URL smoke requires a Miruro episode") } source = try await MiruroResolver.resolve(mediaID: mediaID, episode: episode) } else { source = try ResolvedSource.load(from: file) } let suite = "terebi.resolved-check." + UUID().uuidString let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let clock = Clock() let model = PlayerModel(defaults: defaults, progressFile: CommandLine.arguments.count > 2 ? CommandLine.arguments[2] : nil, skipClock: { clock.now }) defer { model.shutdown() } if input.hasPrefix("https://") { model.openStream(input) } else { model.openResolvedSource(file) } try await until("video/audio decode and advancing playback", model: model) { model.state.status == PLAYER_PLAYING && model.state.has_video && model.state.has_audio && model.state.position > 0.2 } print("PASS: resolved HLS decodes video/audio and advances") fflush(stdout) if input.hasPrefix("https://") { try await until("descriptive title and measured-duration metadata bands", model: model) { model.fileName != source.title && !model.visibleIntervals.isEmpty } let intervals = model.visibleIntervals require(intervals.allSatisfy { $0.start.isFinite && $0.end.isFinite && $0.start >= 0 && $0.end > $0.start && $0.end <= model.state.duration }, "bands use finite in-bounds actual media duration") require(intervals.filter { $0.kind == .intro }.count <= 1 && intervals.filter { $0.kind == .outro }.count <= 1, "at most one band of each kind") print("PASS: Miruro title '\(model.fileName!)' and \(intervals.count) measured-duration bands") } require(model.setPlayback(playing: false), "pause accepted") try await until("pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED } require(model.state.can_seek && model.state.duration > 0, "seekable measured duration") guard let index = source.subtitles.firstIndex(where: \.isDefault) else { fatalError("FAIL: smoke requires a default subtitle") } let track = source.subtitles[index] let document = try await WebVTTDocument.load(url: track.url, headers: track.headers) let playableCues = document.cues.filter { !$0.text.isEmpty && $0.start + 0.1 < model.state.duration } require(playableCues.count >= 2, "smoke requires two visible subtitle cues") let target = playableCues[1].start + min(0.1, (playableCues[1].end - playableCues[1].start) / 2) require(!document.text(at: target).isEmpty, "forward seek targets a visible English cue") require(model.seekRelative(target - model.state.position), "seek accepted") try await until("seek acknowledged and subtitle synchronized", model: model) { !model.state.seek_pending && abs(model.state.position - target) < 0.2 && model.subtitleText == document.text(at: model.state.position) && model.subtitleError == nil } require(model.selectedSubtitle == index && model.subtitleTracks[index].label == track.label, "default selection and label retained") let backward = playableCues[0].start + min(0.1, (playableCues[0].end - playableCues[0].start) / 2) require(model.seekRelative(backward - model.state.position), "backward seek accepted") try await until("backward seek subtitle synchronized", model: model) { !model.state.seek_pending && abs(model.state.position - backward) < 0.2 && model.subtitleText == document.text(at: model.state.position) } print("PASS: default VTT selected, forward/backward seek and acknowledged cue timing") if !input.hasPrefix("https://") { try await segmentSkips(model, clock: clock, file: file) } } static func segmentSkips(_ model: PlayerModel, clock: Clock, file: URL) async throws { let opening = MiruroInterval(kind: .intro, start: 0.5, end: 1.5, episode: 1) let ending = MiruroInterval(kind: .outro, start: 2, end: model.duration, episode: 1) let metadata = MiruroMetadata(title: nil, intervals: [opening, ending]) require(model.segmentToSkip == nil && !model.skipSegment(), "absent metadata cannot skip") require(model.seekRelative(0.75 - model.state.position), "enter opening") try await until("opening position acknowledged", model: model) { !model.state.seek_pending && abs(model.state.position - 0.75) < 0.1 } model.supplyMetadataForCheck(metadata, episode: 1) require(model.segmentToSkip == opening, "late metadata offers opening at acknowledged paused position") clock.now = 102.999 require(model.segmentToSkip == opening, "opening remains available before three seconds") clock.now = 103 require(model.segmentToSkip == nil && !model.skipSegment(), "offer expires at three seconds while paused") clock.now = 104 try await Task.sleep(for: .milliseconds(100)) require(model.segmentToSkip == nil, "polling does not reoffer ignored opening") require(model.seekRelative(2.25 - model.state.position), "enter ending") require(model.segmentToSkip == nil, "pending seek cannot offer stale opening") try await until("ending offer", model: model) { !model.state.seek_pending && model.segmentToSkip == ending } require(model.skipSegment(), "ending skip accepted") require(model.segmentToSkip == nil, "accepted skip immediately removes button") try await until("ending skip reaches exact media end", model: model) { !model.state.seek_pending && abs(model.state.position - ending.end) < 0.1 } require(!model.effectivePlaying, "paused skip does not start playback") require(model.seekRelative(0.75 - model.state.position), "revisit ignored opening") try await until("opening revisit acknowledged", model: model) { !model.state.seek_pending && abs(model.state.position - 0.75) < 0.1 } require(model.segmentToSkip == nil, "ignored opening stays dismissed when revisited") model.closeFile() require(model.segmentToSkip == nil, "close removes offer") model.openResolvedSource(file) try await until("reopened media playing", model: model) { model.state.status == PLAYER_PLAYING && !model.state.seek_pending && !model.isBusy } require(model.setPlayback(playing: false), "pause reopened media") try await until("reopened media paused", model: model) { model.state.status == PLAYER_PAUSED } require(model.seekRelative(0.75 - model.state.position), "reenter opening after reopen") try await until("reopened opening acknowledged", model: model) { !model.state.seek_pending && abs(model.state.position - 0.75) < 0.1 } model.supplyMetadataForCheck(metadata, episode: 1) try await until("new open resets dismissal after seek settles", model: model) { model.segmentToSkip == opening } require(model.setPlayback(playing: true), "resume inside opening") try await until("playing opening offer", model: model) { model.state.status == PLAYER_PLAYING && !model.state.buffering && model.state.position > 0.85 && model.segmentToSkip == opening } require(model.skipSegment(), "opening skip accepted") try await until("opening skip reaches segment end", model: model) { !model.state.seek_pending && abs(model.state.position - opening.end) < 0.1 } require(model.segmentToSkip == nil, "segment end is outside opening") require(model.effectivePlaying, "opening skip preserves playing intent") print("PASS: timed opening/ending offers, dismissal, pending seeks, exact-end skips and reopen reset") } }