fix: separate Cast launch readiness, preserve volume intent and reuse seek inputs

This commit is contained in:
2026-10-13 21:40:19 +02:00
parent 697158aa02
commit b9e63cd760
8 changed files with 365 additions and 55 deletions
+152 -7
View File
@@ -86,6 +86,7 @@ private final class Receiver {
var dropCommands = false
var rejectCommand = false
var holdSeek = false
var holdVolume = false
var ignoreHeartbeat = false
var verifyRelay = false
var relayVerified = false
@@ -164,10 +165,17 @@ private final class Receiver {
} else { socket?.send(content: frame, completion: .contentProcessed { _ in }) }
}
private func receiverStatus(_ requestID: Int) throws {
try send(["type": "RECEIVER_STATUS", "requestId": requestID,
"status": ["applications": [["appId": "CC1AD845", "sessionId": "app-session", "transportId": transport]],
"volume": ["level": volume, "muted": muted]]], namespace: CastWire.receiver, fragmented: true)
func receiverStatus(_ requestID: Int? = nil, appID: String? = "CC1AD845",
level: Double? = nil, source: String? = nil) throws {
let apps: [[String: Any]] = appID.map { [["appId": $0, "sessionId": "app-session", "transportId": transport]] } ?? []
var json: [String: Any] = ["type": "RECEIVER_STATUS",
"status": ["applications": apps, "volume": ["level": level ?? volume, "muted": muted]]]
if let requestID { json["requestId"] = requestID }
try send(json, namespace: CastWire.receiver, source: source, fragmented: true)
}
func receiverPing() throws {
try send(["type": "PING"], namespace: CastWire.heartbeat)
}
func status(requestID: Int = 0, session override: Int? = nil, content: String? = nil,
@@ -241,9 +249,13 @@ private final class Receiver {
try status(requestID: requestID)
case "SET_VOLUME":
require(message.namespace == CastWire.receiver, "Volume controls device, not stream")
if rejectCommand {
try send(["type": "INVALID_REQUEST", "requestId": requestID], namespace: CastWire.receiver)
return
}
volume = (json["volume"] as? [String: Any])?["level"] as? Double ?? volume
muted = (json["volume"] as? [String: Any])?["muted"] as? Bool ?? muted
try receiverStatus(requestID)
if !holdVolume { try receiverStatus(requestID) }
case "STOP":
require(message.namespace == CastWire.media, "Disconnect does not stop another sender's application")
require(json["mediaSessionId"] as? Int == session, "Disconnect stops only owned media")
@@ -437,6 +449,127 @@ struct CastSessionCheck {
require(disconnected && errors.isEmpty && peer.failure == nil, "Safe disconnect without spurious cancellation errors")
}
static func launchRegressions(identity: SecIdentity) async throws {
for mode in ["cold", "asynchronous", "intermediate"] {
let peer = try Receiver(identity: identity)
peer.dropLaunch = true
try await peer.start()
let clock = CastCheckClock()
let session = CastSession(sleep: { try await clock.sleep($0) })
var outcome: String?
let task = Task {
do { try await session.connect(to: peer.device); outcome = "connected" }
catch { outcome = error.localizedDescription }
}
try await until("LAUNCH arrives before delayed receiver status") {
peer.commands.contains { $0.0 == "LAUNCH" } && clock.hasDelay(.seconds(5))
}
let launchID = peer.commands.first { $0.0 == "LAUNCH" }!.1["requestId"] as! Int
if mode == "cold" {
clock.advance(.seconds(9))
try await until("Cold launch advances past ordinary request deadline") {
outcome != nil || peer.commands.contains { $0.0 == "PING" } && clock.hasDelay(.seconds(5))
}
require(outcome == nil, "Cold receiver launch survives the ordinary eight-second control deadline")
}
let pongCount = peer.commands.filter { $0.0 == "PONG" }.count
try peer.receiverStatus(appID: "unrelated-app")
try peer.receiverStatus(source: "unrelated-transport")
if mode == "intermediate" { try peer.receiverStatus(launchID, appID: nil) }
try peer.receiverPing()
try await until("Ordered receiver barrier after unrelated and intermediate launch statuses") {
outcome != nil || peer.commands.filter { $0.0 == "PONG" }.count == pongCount + 1
}
require(outcome == nil, "Launch waits for Default Media Receiver rather than adopting another app or rejecting intermediate status")
if mode == "cold" {
clock.advance(.seconds(11))
try await until("Cold receiver remains pending beyond the TLS connection deadline") {
outcome != nil || peer.commands.filter { $0.0 == "PING" }.count >= 2 && clock.hasDelay(.seconds(5))
}
require(outcome == nil, "Successful TLS connection does not impose a twelve-second receiver launch deadline")
}
try peer.receiverStatus() // Real receivers can broadcast readiness without the LAUNCH request ID.
try await until("Asynchronous receiver launch status completes connection") { outcome != nil }
require(outcome == "connected", "Default Media Receiver readiness completes launch without the original acknowledgement")
await task.value
try peer.receiverStatus(launchID) // The original acknowledgement can arrive after readiness.
try await session.load(media(), title: "Delayed launch", position: 0, playing: false, subtitle: nil)
require(peer.failure == nil, "Late launch acknowledgement does not disturb acquired receiver ownership")
session.disconnect(stopReceiver: false)
try await until("Delayed launch cancels all clock handles") { clock.pendingCount == 0 }
peer.stop()
}
}
static func volumeRegressions(identity: SecIdentity) async throws {
let peer = try Receiver(identity: identity)
peer.holdVolume = true
try await peer.start()
defer { peer.stop() }
let clock = CastCheckClock()
let session = CastSession(sleep: { try await clock.sleep($0) })
var statuses: [CastStatus] = []
var errors: [String] = []
session.onStatus = { statuses.append($0) }
session.onError = { errors.append($0) }
try await session.connect(to: peer.device)
try await session.load(media(), title: "Volume feedback", position: 0, playing: false, subtitle: nil)
func volumeID(_ index: Int) -> Int {
peer.commands.filter { $0.0 == "SET_VOLUME" }[index].1["requestId"] as! Int
}
func barrier(_ message: String) async throws {
let pongs = peer.commands.filter { $0.0 == "PONG" }.count
try peer.status()
try peer.receiverPing()
try await until(message) { peer.commands.filter { $0.0 == "PONG" }.count == pongs + 1 }
}
require(session.setVolume(0.2) && session.setVolume(0.8), "Rapid slider commands accepted")
try await until("Both slider commands reach receiver") { peer.commands.filter { $0.0 == "SET_VOLUME" }.count == 2 }
let beforeBurst = statuses.count
try peer.receiverStatus(volumeID(0), level: 0.2)
let launchID = peer.commands.first { $0.0 == "LAUNCH" }!.1["requestId"] as! Int
try peer.receiverStatus(launchID, level: 0.1)
try peer.receiverStatus(level: 0.8)
try peer.receiverStatus(level: 0.45)
try await barrier("Media status provides barrier after stale volume feedback")
require(statuses.last?.volume == 0.8 && statuses.dropFirst(beforeBurst).allSatisfy { $0.volume == 0.8 },
"Older command and unsolicited receiver feedback cannot pull the slider away from latest user intent")
try peer.receiverStatus(volumeID(1), level: 0.79)
try await barrier("Latest slider acknowledgement supplies receiver-authoritative volume")
require(statuses.last?.volume == 0.79, "Latest acknowledgement restores the receiver's actual quantized volume")
try peer.receiverStatus(volumeID(0), level: 0.2)
try peer.receiverStatus(launchID, level: 0.1)
try await barrier("Late duplicate old volume acknowledgement is consumed")
require(statuses.last?.volume == 0.79, "Old acknowledged request cannot roll volume backward after latest command settles")
try peer.receiverStatus(level: 0.4)
try await barrier("External TV volume arrives while idle")
require(statuses.last?.volume == 0.4, "External TV changes remain authoritative when no latest slider intent is pending")
require(session.setVolume(0.6) && session.setVolume(0.9), "Second overlapping slider burst accepted")
try await until("Second slider burst reaches receiver") { peer.commands.filter { $0.0 == "SET_VOLUME" }.count == 4 }
try peer.receiverStatus(volumeID(3), level: 0.88)
try peer.receiverStatus(volumeID(2), level: 0.6)
try peer.receiverStatus(level: 0.5)
try await barrier("Reverse acknowledgement order and external change processed")
require(statuses.last?.volume == 0.5, "Settled latest intent allows external updates even after an older overlapping request")
require(session.setVolume(0.7), "Unanswered volume command accepted")
try await until("Volume timeout waits on controlled clock") {
peer.commands.filter { $0.0 == "SET_VOLUME" }.count == 5 && clock.hasDelay(.seconds(8))
}
clock.advance(.seconds(8))
try await until("Volume timeout releases latest user intent") { !errors.isEmpty }
require(statuses.last?.volume == 0.5, "Timed-out volume intent restores the last authoritative receiver value")
try peer.receiverStatus(level: 0.7)
try await barrier("External volume becomes authoritative after timeout")
require(statuses.last?.volume == 0.7, "Unsolicited TV volume resumes after bounded command failure")
peer.rejectCommand = true
let beforeRejection = errors.count
require(session.setVolume(0.4), "Rejected volume command is sent")
try await until("Explicit volume rejection releases latest intent") { errors.count > beforeRejection }
require(statuses.last?.volume == 0.7, "Rejected slider intent restores authoritative receiver volume")
session.disconnect(stopReceiver: false)
try await until("Volume regression cancels all clock handles") { clock.pendingCount == 0 }
}
static func connectionDeadline() async throws {
let listener = try NWListener(using: .tcp, on: .any)
@MainActor final class ListenerState {
@@ -494,9 +627,9 @@ struct CastSessionCheck {
} else if mode == "launch-timeout" {
let task = Task { try await session.connect(to: peer.device) }
try await until("Receiver launch waits on frozen clock") {
peer.commands.contains { $0.0 == "LAUNCH" } && clock.hasDelay(.milliseconds(400))
peer.commands.contains { $0.0 == "LAUNCH" } && clock.hasDelay(.seconds(30))
}
clock.advance(.milliseconds(400))
clock.advance(.seconds(30))
do { try await task.value; fatalError("FAIL: unanswered launch succeeded") }
catch CastError.timeout { require(disconnected, "Launch deadline releases channel") }
} else {
@@ -623,11 +756,23 @@ struct CastSessionCheck {
try codec()
let (identity, directory) = try testIdentity()
defer { try? FileManager.default.removeItem(at: directory) }
if CommandLine.arguments.count == 2, CommandLine.arguments[1] == "--launch-regressions" {
try await launchRegressions(identity: identity)
print("PASS: cold and asynchronous Cast launch readiness, unrelated app protection, late acknowledgement")
return
}
if CommandLine.arguments.count == 2, CommandLine.arguments[1] == "--volume-regressions" {
try await volumeRegressions(identity: identity)
print("PASS: latest slider intent, stale volume feedback, authoritative acknowledgement, external TV changes, timeout")
return
}
if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--playback" {
try await playback(identity: identity, executable: CommandLine.arguments[2], file: CommandLine.arguments[3])
print("PASS: native TLS receiver fetches relay and acknowledges PlayerModel handoff/control/disconnect")
} else {
try await behavior(identity: identity)
try await launchRegressions(identity: identity)
try await volumeRegressions(identity: identity)
try await failures(identity: identity)
try await connectionDeadline()
print("PASS: bounded Cast framing, self-signed TLS launch/load/control, correlation, stale ownership, queue, deadlines, heartbeat, cancellation")