Corregge lifecycle RTMP/preview tra partite, reset tabellone, orientamento con debounce, condivisione su iPad e script test_ios_simulator per CI locale. Co-authored-by: Cursor <cursoragent@cursor.com>
112 lines
3.4 KiB
Swift
112 lines
3.4 KiB
Swift
import Foundation
|
|
|
|
@MainActor
|
|
final class SessionCableService: ObservableObject {
|
|
@Published private(set) var connected = false
|
|
|
|
var onScoreUpdate: ((ScoreState) -> Void)?
|
|
var onPauseStream: (() -> Void)?
|
|
var onResumeStream: (() -> Void)?
|
|
var onStopStream: (() -> Void)?
|
|
|
|
private var client: ActionCableClient?
|
|
private var intentionalDisconnect = false
|
|
private var reconnectAttempt = 0
|
|
private var reconnectTask: Task<Void, Never>?
|
|
private var sessionId: String?
|
|
private var accessToken: String?
|
|
private var deviceRole = "camera"
|
|
|
|
func connect(sessionId: String, accessToken: String, deviceRole: String = "camera") {
|
|
disconnect()
|
|
self.sessionId = sessionId
|
|
self.accessToken = accessToken
|
|
self.deviceRole = deviceRole
|
|
intentionalDisconnect = false
|
|
reconnectAttempt = 0
|
|
openSocket()
|
|
}
|
|
|
|
func disconnect() {
|
|
intentionalDisconnect = true
|
|
reconnectTask?.cancel()
|
|
reconnectTask = nil
|
|
client?.disconnect()
|
|
client = nil
|
|
connected = false
|
|
sessionId = nil
|
|
accessToken = nil
|
|
}
|
|
|
|
func sendScoreUpdate(_ score: ScoreState) {
|
|
client?.performReceive(payload: score.cablePayload())
|
|
}
|
|
|
|
private func openSocket() {
|
|
guard let sessionId, let accessToken else { return }
|
|
let cable = ActionCableClient(cableUrl: AppConfig.cableUrl, accessToken: accessToken)
|
|
cable.connect(
|
|
channel: "SessionChannel",
|
|
params: ["session_id": sessionId, "device_role": deviceRole],
|
|
listener: self
|
|
)
|
|
client = cable
|
|
}
|
|
|
|
private func scheduleReconnect() {
|
|
guard !intentionalDisconnect else { return }
|
|
reconnectAttempt += 1
|
|
let delay = min(30.0, pow(2.0, Double(min(reconnectAttempt - 1, 4))))
|
|
reconnectTask?.cancel()
|
|
reconnectTask = Task {
|
|
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
|
|
guard !Task.isCancelled, !intentionalDisconnect else { return }
|
|
openSocket()
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SessionCableService: ActionCableClient.Listener {
|
|
nonisolated func onConnected() {
|
|
Task { @MainActor in
|
|
connected = true
|
|
reconnectAttempt = 0
|
|
}
|
|
}
|
|
|
|
nonisolated func onDisconnected() {
|
|
Task { @MainActor in
|
|
connected = false
|
|
scheduleReconnect()
|
|
}
|
|
}
|
|
|
|
nonisolated func onChannelMessage(_ payload: [String: Any]) {
|
|
Task { @MainActor in
|
|
let type = payload["type"] as? String ?? ""
|
|
switch type {
|
|
case "score_update":
|
|
onScoreUpdate?(ScoreState.fromCablePayload(payload))
|
|
case "command":
|
|
let action = payload["action"] as? String ?? ""
|
|
switch action {
|
|
case "pause_stream": onPauseStream?()
|
|
case "resume_stream": onResumeStream?()
|
|
case "stop_stream": onStopStream?()
|
|
default: break
|
|
}
|
|
case "stream_event":
|
|
let event = payload["event"] as? String ?? ""
|
|
switch event {
|
|
case "paused": onPauseStream?()
|
|
case "resumed": onResumeStream?()
|
|
case "ended": onStopStream?()
|
|
default: break
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|