Fix regia basket e stabilità diretta iOS.

La regia remota non va più in 500 su basket/timed; iOS gestisce chiusura set, riconnessione RTMP e avanzamento quarti in modo affidabile.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Emiliano Frascaro
2026-06-20 12:30:34 +02:00
co-authored by Cursor
parent 731b43ea88
commit 270bf680a4
16 changed files with 665 additions and 441 deletions
@@ -42,6 +42,8 @@ final class LiveBroadcastEngine: ObservableObject {
private var broadcastGeneration = 0
private var suppressDisconnectError = false
private var orientationTransitionUntil: Date = .distantPast
private var reconnectAttempts = 0
private var reconnectTask: Task<Void, Never>?
private var lastAppliedCaptureOrientation: AVCaptureVideoOrientation?
private var orientationDebounceTask: Task<Void, Never>?
private var rtmpSession: (any Session)?
@@ -121,6 +123,9 @@ final class LiveBroadcastEngine: ObservableObject {
self.suppressDisconnectError = suppressDisconnectError
defer { self.suppressDisconnectError = false }
broadcastGeneration += 1
reconnectTask?.cancel()
reconnectTask = nil
reconnectAttempts = 0
publishPending = false
publishTask?.cancel()
publishTask = nil
@@ -130,6 +135,9 @@ final class LiveBroadcastEngine: ObservableObject {
func resumeBroadcast(config: BroadcastConfig) async throws {
self.config = config
reconnectAttempts = 0
reconnectTask?.cancel()
reconnectTask = nil
publishPending = true
if !pipelineConfigured {
try await configurePipeline(config)
@@ -144,6 +152,9 @@ final class LiveBroadcastEngine: ObservableObject {
self.suppressDisconnectError = suppressDisconnectError
defer { self.suppressDisconnectError = false }
broadcastGeneration += 1
reconnectTask?.cancel()
reconnectTask = nil
reconnectAttempts = 0
publishTask?.cancel()
publishTask = nil
publishPending = false
@@ -254,10 +265,15 @@ final class LiveBroadcastEngine: ObservableObject {
try await publish(config: config, generation: generation)
if generation == broadcastGeneration {
publishPending = false
reconnectAttempts = 0
}
} catch {
guard generation == broadcastGeneration else { return }
publishPending = false
if phase == .reconnecting || reconnectAttempts > 0 {
scheduleReconnect()
return
}
let message = UserFacingError.message(for: error) ?? "Connessione RTMP fallita"
setPhase(.error, error: message)
}
@@ -300,16 +316,16 @@ final class LiveBroadcastEngine: ObservableObject {
try await session.connect { [weak self] in
Task { @MainActor in
guard let self,
!self.shouldSuppressDisconnectError,
self.broadcastGeneration == generation,
self.phase == .live else { return }
self.setPhase(.error, error: "Connessione RTMP interrotta")
self.phase == .live || self.phase == .reconnecting else { return }
self.scheduleReconnect()
}
}
guard generation == broadcastGeneration else {
await teardownRTMP(keepPreview: true)
return
}
reconnectAttempts = 0
setPhase(.live)
}
@@ -341,9 +357,8 @@ final class LiveBroadcastEngine: ObservableObject {
switch state {
case .open:
setPhase(.live)
case .closed where phase == .live:
guard !shouldSuppressDisconnectError else { return }
setPhase(.error, error: "Connessione RTMP interrotta")
case .closed where phase == .live || phase == .reconnecting:
scheduleReconnect()
default:
break
}
@@ -369,6 +384,33 @@ final class LiveBroadcastEngine: ObservableObject {
suppressDisconnectError || Date() < orientationTransitionUntil
}
/// Riconnessione automatica RTMP (come Android): evita falsi errori su iPad.
private func scheduleReconnect() {
guard !shouldSuppressDisconnectError else { return }
guard phase == .live || phase == .reconnecting else { return }
guard let config else { return }
reconnectAttempts += 1
if reconnectAttempts > config.maxReconnectAttempts {
reconnectTask = nil
setPhase(.error, error: "Connessione RTMP interrotta")
return
}
setPhase(.reconnecting)
reconnectTask?.cancel()
let generation = broadcastGeneration
let delayMs = config.reconnectDelayMs
reconnectTask = Task {
try? await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
guard !Task.isCancelled, generation == broadcastGeneration else { return }
guard phase == .reconnecting else { return }
publishPending = true
publishInFlight = false
await startPendingPublish()
}
}
private func applyVideoOrientationIfNeeded(force: Bool = false) async {
let orientation = BroadcastVideoOrientation.captureOrientation()
let landscapeLocked = AppOrientation.mode == .landscape