Files
MatchLiveTv/native/ios/MatchLiveTv/Data/Cable/SessionCableService.swift
Emiliano Frascaro 731b43ea88 Stabilizza diretta iOS e aggiunge test simulatore pre-release.
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>
2026-06-20 12:02:14 +02:00

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
}
}
}
}