Aggiunge il mute del microfono in diretta per evitare claim YouTube sulla musica in palestra.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-12 22:38:05 +02:00
co-authored by Cursor
parent bc2257efd2
commit 1fecccaafd
53 changed files with 827 additions and 25 deletions
@@ -31,6 +31,8 @@ struct BroadcastControlsOverlay: View {
var onCloseSet: (() -> Void)?
var onAdvancePeriod: (() -> Void)?
let onPauseOrResume: () -> Void
let onToggleAudioMute: () -> Void
let audioMuted: Bool
let onTerminate: () -> Void
let onShareLive: () -> Void
let onShareRegia: () -> Void
@@ -69,7 +71,8 @@ struct BroadcastControlsOverlay: View {
targetFps: targetFps,
bitrateKbps: bitrateKbps,
networkType: networkType,
deviceHealth: deviceHealth
deviceHealth: deviceHealth,
audioMuted: audioMuted
)
}
.padding(.trailing, 8)
@@ -142,6 +145,12 @@ struct BroadcastControlsOverlay: View {
action: onPauseOrResume,
highlighted: isPaused
)
SideIconButton(
systemName: audioMuted ? "speaker.slash.fill" : "speaker.wave.2.fill",
accessibilityLabel: audioMuted ? L10n.t("broadcast.unmute.cd") : L10n.t("broadcast.mute.cd"),
action: onToggleAudioMute,
highlighted: audioMuted
)
SideIconButton(
systemName: "stop.fill",
accessibilityLabel: L10n.t("broadcast.terminate.cd"),
@@ -195,6 +204,7 @@ private struct BroadcastTelemetryPanel: View {
let bitrateKbps: Int
let networkType: String
let deviceHealth: DeviceHealth
let audioMuted: Bool
var body: some View {
VStack(alignment: .trailing, spacing: 2) {
@@ -214,6 +224,11 @@ private struct BroadcastTelemetryPanel: View {
Text("\(networkType) · \(deviceHealth.batteryPercent)%")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(MatchColors.textSecondary)
if audioMuted {
Text(L10n.t("broadcast.audio.muted.label"))
.font(.system(size: 11, weight: .bold))
.foregroundStyle(MatchColors.accentYellow)
}
ThermalIndicator(state: deviceHealth.thermalState)
}
.padding(.horizontal, 10)
@@ -27,6 +27,7 @@ struct BroadcastScreen: View {
@State private var logoReady = 0
@State private var pauseInFlight = false
@State private var resumeInFlight = false
@State private var muteInFlight = false
@State private var deviceHealth = DeviceTelemetry.snapshot()
@State private var snackbarMessage: String?
@State private var shareItem: ShareItem?
@@ -188,6 +189,7 @@ struct BroadcastScreen: View {
let metrics = broadcastCoordinator.metrics
let score = scoreController.score
let isPaused = session.isPaused || metrics.phase == .paused
let audioMuted = session.audioMuted || metrics.audioMuted
let statusText = broadcastStatusText(isPaused: isPaused, metrics: metrics)
let statusColor = broadcastStatusColor(isPaused: isPaused, metrics: metrics)
let boardType = match.boardType.isEmpty ? score.boardType : match.boardType
@@ -282,6 +284,10 @@ struct BroadcastScreen: View {
pauseStream()
}
},
onToggleAudioMute: {
Task { await setAudioMuted(!audioMuted, remote: false) }
},
audioMuted: audioMuted,
onTerminate: { Task { await stopStream() } },
onShareLive: { shareLiveLink(session: session, subject: shareSubject) },
onShareRegia: { shareRegiaLink(subject: shareSubject) },
@@ -384,6 +390,7 @@ struct BroadcastScreen: View {
config: config,
paused: loaded.isPaused
)
await container.broadcastCoordinator.setAudioMuted(loaded.audioMuted)
}
} catch {
guard generation == bootstrapGeneration else { return }
@@ -418,6 +425,12 @@ struct BroadcastScreen: View {
container.sessionCable.onStopStream = { [weak container] in
Task { @MainActor in await stopStream() }
}
container.sessionCable.onAudioMute = { muted in
Task { @MainActor in
guard !muteInFlight else { return }
await setAudioMuted(muted, remote: true)
}
}
container.sessionCable.connect(sessionId: sessionId, accessToken: token)
}
@@ -498,6 +511,7 @@ struct BroadcastScreen: View {
try await container.broadcastCoordinator.resumeBroadcast(config: config)
updated = try await container.sessionRepository.fetchSession(id: sessionId)
session = updated
await container.broadcastCoordinator.setAudioMuted(updated.audioMuted)
snackbarMessage = L10n.t("broadcast.snackbar.resumed")
} catch {
snackbarMessage = L10n.t("broadcast.snackbar.resume.error", UserFacingError.message(for: error) ?? L10n.t("common.error.generic"))
@@ -516,6 +530,31 @@ struct BroadcastScreen: View {
snackbarMessage = L10n.t("broadcast.snackbar.paused.remote")
}
private func setAudioMuted(_ muted: Bool, remote: Bool) async {
guard !muteInFlight else { return }
if (session?.audioMuted == true || broadcastCoordinator.metrics.audioMuted) == muted {
await container.broadcastCoordinator.setAudioMuted(muted)
return
}
muteInFlight = true
defer { muteInFlight = false }
do {
if remote {
session = session?.withAudioMuted(muted)
} else {
session = try await container.sessionRepository.setAudioMute(id: sessionId, muted: muted)
}
await container.broadcastCoordinator.setAudioMuted(muted)
snackbarMessage = L10n.t(
remote
? (muted ? "broadcast.snackbar.muted.remote" : "broadcast.snackbar.unmuted.remote")
: (muted ? "broadcast.snackbar.muted" : "broadcast.snackbar.unmuted")
)
} catch {
snackbarMessage = L10n.t("broadcast.snackbar.mute.error", UserFacingError.message(for: error) ?? L10n.t("common.error.generic"))
}
}
/// Ripresa dalla regia / echo cable: solo RTMP locale, senza PATCH resume.
private func applyRemoteResume(container: AppContainer) async {
guard !resumeInFlight else { return }
@@ -528,7 +567,8 @@ struct BroadcastScreen: View {
let config = broadcastConfig(for: current, rtmpUrl: url)
thermalManager.updateBaseline(config)
try await container.broadcastCoordinator.resumeBroadcast(config: config)
snackbarMessage = L10n.t("broadcast.snackbar.resumed")
await container.broadcastCoordinator.setAudioMuted(current.audioMuted)
snackbarMessage = L10n.t("broadcast.snackbar.resumed.remote")
} catch {
snackbarMessage = L10n.t("broadcast.snackbar.resume.rtmp.error", UserFacingError.message(for: error) ?? L10n.t("common.error.generic"))
}
@@ -567,6 +607,7 @@ struct BroadcastScreen: View {
container.sessionCable.onPauseStream = nil
container.sessionCable.onResumeStream = nil
container.sessionCable.onStopStream = nil
container.sessionCable.onAudioMute = nil
container.sessionCable.disconnect()
await container.broadcastCoordinator.stopBroadcast(sessionId: sessionId)
}