70 lines
4.6 KiB
Swift
70 lines
4.6 KiB
Swift
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 main() async throws {
|
|
_ = NSApplication.shared
|
|
NSApp.setActivationPolicy(.prohibited)
|
|
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() }
|
|
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 }
|
|
require(model.seekRelative(5) && model.seekRelative(10), "rapid remote skips accumulate before acknowledgement")
|
|
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")
|
|
}
|
|
}
|