import AppKit @main @MainActor struct CastPlaybackCheck { static func require(_ condition: @autoclosure () -> Bool, _ message: String) { if !condition() { fatalError("FAIL: " + message) } } // A real I/O watchdog, not a clock used by the playback behavior under test. static func until(_ model: PlayerModel, _ message: String, permitError: Bool = false, _ predicate: () -> Bool) async throws { let deadline = ContinuousClock.now.advanced(by: .seconds(45)) while !predicate(), ContinuousClock.now < deadline { if !permitError, let error = model.commandError { fatalError("FAIL: " + error) } try await Task.sleep(for: .milliseconds(20)) } require(predicate(), message) } static func interrupted(_ model: PlayerModel, source: URL, port: UInt16, scenario: String, gates: URL, defaults: UserDefaults) async throws { func signal(_ name: String) throws { try Data().write(to: gates.appendingPathComponent(name), options: .atomic) } func awaitSignal(_ name: String) async throws { try await until(model, "\(scenario): fixture signals \(name)", permitError: true) { FileManager.default.fileExists(atPath: gates.appendingPathComponent(name).path) } } let replacement = gates.appendingPathComponent("replacement.mp4") try FileManager.default.copyItem(at: source, to: replacement) let active = scenario.hasSuffix("-active") let expectedError = scenario == "reject-load" || scenario == "disconnect-active" model.setVolume(0) model.open(source) try await until(model, "interruption source plays") { model.state.status == PLAYER_PLAYING && model.canSeek } require(model.setPlayback(playing: false), "initial local pause accepted") try await until(model, "initial local pause acknowledged") { model.state.status == PLAYER_PAUSED && model.playbackIntent == nil } require(model.seekRelative(9.25 - model.state.position), "initial local position accepted") try await until(model, "initial local position acknowledged") { !model.state.seek_pending && abs(model.state.position - 9.25) < 0.15 } if !active { require(model.setPlayback(playing: true), "local playback starts before pending handoff") try await until(model, "local playback acknowledged before handoff") { model.state.status == PLAYER_PLAYING && model.playbackIntent == nil } } let beforeConnect = model.state.position model.cast(to: CastDevice(id: "loopback", name: "Loopback TV", host: "127.0.0.1", port: port)) if active { try await until(model, "active receiver acknowledges handoff") { model.castDevice != nil && !model.castConnecting && model.canSeek && model.state.status == PLAYER_PAUSED } require(abs(model.state.position - 9.25) < 0.15, "active receiver starts at acknowledged local position") require(model.volume == 1, "active receiver volume is visible before local rollback") if scenario == "disconnect-active" { require(model.setPlayback(playing: true), "initial receiver PLAY accepted") try await until(model, "initial receiver PLAY acknowledged") { model.state.status == PLAYER_PLAYING && model.playbackIntent == nil } } try signal("active") } try await awaitSignal("held") if !active { require(model.castDevice != nil && model.castConnecting, "held handoff remains pending") if scenario == "cancel-connect" { try await until(model, "pending connection does not pause local playback") { model.state.status == PLAYER_PLAYING && model.state.position > beforeConnect } } else { try await until(model, "pending LOAD follows acknowledged local pause") { model.state.status == PLAYER_PAUSED } } } if scenario == "reject-load" { try signal("observed") try await until(model, "LOAD rejection restores original local playback", permitError: true) { model.castDevice == nil && !model.castConnecting && model.commandError != nil && model.state.status == PLAYER_PLAYING && model.canSeek } } else { switch scenario { case "cancel-connect", "cancel-load": model.stopCasting() case "replace-load", "replace-active": model.open(replacement) case "close-load", "close-active": model.closeFile() case "shutdown-load", "shutdown-active": model.shutdown() case "disconnect-active": require(model.setPlayback(playing: false) && model.setPlayback(playing: true) && model.setPlayback(playing: false), "latest unacknowledged receiver PAUSE accepted") require(model.playbackIntent == false && model.state.status == PLAYER_PLAYING, "latest intent remains distinct from acknowledged receiver playback") require(model.seekRelative(10), "receiver seek preview accepted before disconnect") require(model.state.seek_pending, "receiver seek remains pending before disconnect") default: fatalError("Unknown interruption scenario: " + scenario) } try signal("interrupted") } try await awaitSignal("released") if scenario.hasPrefix("shutdown-") { require(model.castDevice == nil && !model.castConnecting && !model.canOpen && !model.canClose, "shutdown releases Cast ownership and permanently disables the local player") require(!model.setPlayback(playing: true) && !model.seekRelative(5) && model.volume == 0, "late receiver traffic cannot resume a destroyed player or replace local volume") print("PASS: " + scenario) return } if scenario == "disconnect-active" { try await until(model, "disconnect restores latest intent at acknowledged position", permitError: true) { model.castDevice == nil && !model.castConnecting && !model.isBusy && model.state.status == PLAYER_PAUSED && !model.state.seek_pending && model.playbackIntent == nil && abs(model.state.position - 9.25) < 0.15 } require(abs((PlaybackProgress(defaults: defaults).position(for: source) ?? 0) - 9.25) < 0.15, "unacknowledged remote seek preview never replaces saved acknowledged position") } else if scenario.hasPrefix("close-") { try await until(model, "close acknowledges empty decoder after late receiver traffic") { model.state.status == PLAYER_EMPTY && model.fileName == nil && model.castDevice == nil && !model.isBusy } require(model.playbackIntent == nil, "close does not leave an accidental resume intent") model.open(replacement) } else if scenario.hasPrefix("replace-") { try await until(model, "replacement acknowledges its own local playback after late receiver traffic") { model.castDevice == nil && !model.castConnecting && !model.isBusy && model.fileName == replacement.lastPathComponent && model.state.status == PLAYER_PLAYING && model.canSeek } } else { try await until(model, "cancelled or rejected handoff leaves usable local playback", permitError: expectedError) { model.castDevice == nil && !model.castConnecting && model.state.status == PLAYER_PLAYING && model.canSeek } } require(model.volume == 0, "every interrupted handoff restores local volume") if scenario != "disconnect-active" { // A native acknowledgement is the barrier after released late receiver traffic; // never sleep and infer that stale callbacks did nothing. try await until(model, "local decoder is usable after interruption", permitError: expectedError) { model.canSeek && !model.isBusy } require(model.setPlayback(playing: false), "post-interruption local pause accepted") try await until(model, "post-interruption local pause acknowledged", permitError: expectedError) { model.state.status == PLAYER_PAUSED && model.playbackIntent == nil } require(model.seekRelative(4.25 - model.state.position), "post-interruption local seek accepted") try await until(model, "local seek acknowledges after obsolete receiver replies", permitError: expectedError) { !model.state.seek_pending && abs(model.state.position - 4.25) < 0.15 } require(model.castDevice == nil && !model.castConnecting && model.volume == 0, "obsolete receiver callbacks cannot reclaim playback or volume") if scenario.hasPrefix("replace-") || scenario.hasPrefix("close-") { require(model.fileName == replacement.lastPathComponent, "interruption preserves replacement media identity") } } model.closeFile() try await until(model, "interrupted scenario closes all playback", permitError: expectedError) { model.state.status == PLAYER_EMPTY && model.castDevice == nil && !model.castConnecting } print("PASS: " + scenario) } static func main() async throws { _ = NSApplication.shared NSApp.setActivationPolicy(.prohibited) require(castVolumeSliderFeedbackCheck(), "Native slider keeps user position during tracking and applies receiver volume when idle") if CommandLine.arguments.count == 2, CommandLine.arguments[1] == "--slider-regression" { print("PASS: native volume slider tracking preserves user input and idle receiver feedback") return } let source = URL(fileURLWithPath: CommandLine.arguments[1]) let port = UInt16(CommandLine.arguments[2])! let suite = "terebi.cast-playback-check." + UUID().uuidString let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let model = PlayerModel(defaults: defaults) defer { model.shutdown() } if CommandLine.arguments.count == 5 { try await interrupted(model, source: source, port: port, scenario: CommandLine.arguments[3], gates: URL(fileURLWithPath: CommandLine.arguments[4]), defaults: defaults) return } model.setVolume(0) model.open(source) try await until(model, "local source plays") { model.state.status == PLAYER_PLAYING && model.canSeek } require(model.setPlayback(playing: false), "local pause accepted") try await until(model, "local pause acknowledged") { model.state.status == PLAYER_PAUSED && model.playbackIntent == nil } require(model.seekRelative(9.25 - model.state.position), "local restoration position accepted") require(!model.canCast, "Cast cannot transfer an unacknowledged local seek") try await until(model, "local position acknowledged") { !model.state.seek_pending && abs(model.state.position - 9.25) < 0.15 } model.cast(to: CastDevice(id: "loopback", name: "Loopback TV", host: "127.0.0.1", port: port)) try await until(model, "receiver load acknowledges paused handoff") { model.castDevice != nil && !model.castConnecting && model.canSeek && model.state.status == PLAYER_PAUSED } require(abs(model.state.position - 9.25) < 0.15, "Cast starts at acknowledged local position") require(!model.canStepFrame && model.bufferedRange == nil, "local decoder-only controls are not exposed while casting") require(model.setPlayback(playing: true), "remote play accepted") try await until(model, "receiver play acknowledged") { model.state.status == PLAYER_PLAYING && model.playbackIntent == nil } for _ in 0..<64 { require(model.seekRelative(0.125), "rapid remote skips accumulate beyond queue capacity") } require(model.seekRelative(7), "final remote skip retains accumulated target") try await until(model, "receiver seek acknowledged") { !model.state.seek_pending && abs(model.state.position - 24.25) < 0.15 } require(model.setPlayback(playing: false), "remote pause accepted") try await until(model, "receiver pause acknowledged") { model.state.status == PLAYER_PAUSED && model.playbackIntent == nil } model.stopCasting() try await until(model, "local decoder restores receiver position paused") { model.castDevice == nil && model.state.status == PLAYER_PAUSED && !model.state.seek_pending && abs(model.state.position - 24.25) < 0.15 && !model.isBusy } require(abs((PlaybackProgress(defaults: defaults).position(for: source) ?? 0) - 24.25) < 0.15, "remote history is not overwritten by the abandoned local position") require(model.volume == 0, "local volume survives receiver volume changes") require(model.setPlayback(playing: true), "resume local playback before a failed Cast connection") try await until(model, "local resume acknowledged") { model.state.status == PLAYER_PLAYING && model.playbackIntent == nil } model.cast(to: CastDevice(id: "invalid", name: "Unavailable TV", host: "127.0.0.1", port: 0)) try await until(model, "failed Cast connection restores local playback", permitError: true) { model.castDevice == nil && model.commandError != nil && model.state.status == PLAYER_PLAYING } require(model.canSeek && model.state.position >= 24, "connection failure preserves a usable local timeline") model.closeFile() try await until(model, "closed media releases Cast and decoder") { model.state.status == PLAYER_EMPTY } print("PASS: real local decode → TLS Cast LOAD/play/seek/pause → local restoration/history; failed connection recovery") } }