# iOS — mute audio in diretta (allineamento da Mac) Documento operativo per **Cursor su Mac**: verificare e, se serve, ritoccare l’app iOS dopo il ramo `feature/stream-audio-mute`. **Aggiornato:** 2026-08-12 **Branch:** `feature/stream-audio-mute` (parte da `main`) **Android di riferimento:** mute microfono in overlay + sync regia **API produzione:** `https://www.matchlivetv.it` Il codice iOS è **già nel branch** (engine, overlay, API, Cable, L10n). Su Mac va **compilato, testato su device** e, se HaishinKit 2.2.5 rifiuta un parametro, adattato come sotto. --- ## Perché esiste In palestra la musica di sottofondo finisce nel microfono del telefono. Il relay YouTube fa `-c:a copy`, quindi YouTube sente quella musica e può reclamare il copyright. Il mute **non ferma** lo stream: continua video + overlay + AAC, ma i sample audio sono silenzio. --- ## Contratto API / Cable (già in backend) | Pezzo | Dettaglio | |-------|-----------| | Campo sessione | `audio_muted` (boolean, default `false`) in JSON sessione e `GET/POST /regia/:token/status.json` | | REST app | `PATCH /api/v1/sessions/:id/audio_mute` body `{ "muted": true \| false }` | | REST regia | `POST /regia/:token/audio_mute` body `{ "muted": true \| false }` (senza body: toggle) | | Cable command | `{ "type": "command", "action": "mute_audio" \| "unmute_audio", "muted": true \| false }` | | Cable event | `{ "type": "stream_event", "event": "audio_muted", "muted": true \| false }` | Idempotente: se lo stato è già quello richiesto, il backend non reinvia eventi. --- ## File iOS già modificati ``` native/ios/MatchLiveTv/Streaming/BroadcastModels.swift # BroadcastMetrics.audioMuted native/ios/MatchLiveTv/Streaming/LiveBroadcastEngine.swift # setAudioMuted + AudioMixerSettings.isMuted native/ios/MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift # setAudioMuted native/ios/MatchLiveTv/Domain/Models.swift # StreamSession.audioMuted + withAudioMuted native/ios/MatchLiveTv/Data/API/ApiDtos.swift # audioMuted + AudioMuteRequest native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift # setAudioMute native/ios/MatchLiveTv/Data/Repository/SessionRepository.swift native/ios/MatchLiveTv/Data/Cable/SessionCableService.swift # onAudioMute + AudioMuteCommandLogic native/ios/MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift native/ios/MatchLiveTv/UI/Broadcast/BroadcastScreen.swift native/ios/MatchLiveTv/Core/AppLanguage.swift # IT/EN/FR/DE/ES native/ios/MatchLiveTvTests/LiveBroadcastCoordinatorTests.swift ``` --- ## Engine HaishinKit 2.2.5 (punto da verificare su Mac) Implementazione attesa in `LiveBroadcastEngine`: ```swift func setAudioMuted(_ muted: Bool) async { audioMuted = muted await applyAudioMute() metrics.audioMuted = muted emitMetrics() } private func applyAudioMute() async { guard pipelineConfigured else { return } var settings = await mixer.audioMixerSettings settings.isMuted = audioMuted var track = settings.tracks[0] ?? AudioMixerTrackSettings(downmix: true, channelMap: [0]) track.isMuted = audioMuted settings.tracks[0] = track try? await mixer.setAudioMixerSettings(settings) } ``` In `configurePipeline` i settings iniziali passano `isMuted: audioMuted` sia sul mixer sia sulla track 0. **Se il build fallisce:** 1. `AudioMixerSettings` potrebbe non avere `isMuted` nel memberwise usato oggi — in quel caso muta solo `settings.tracks[0].isMuted`. 2. `setAudioMixerSettings` è `async throws` nel codice attuale; se la signature è sync, togli `try await`. 3. Non staccare il microfono (`attachAudio(nil)`): YouTube/MediaMTX vogliono AAC continuo. Mute = silenzio, non assenza di traccia. Dopo `prepareBroadcast` / `resumeBroadcast` la schermata richiama `setAudioMuted(session.audioMuted)` per riallineare lo stato persistito. --- ## UI overlay Toolbar destra (come Android), sotto pausa: | Stato | SF Symbol | Chiave L10n | |-------|-----------|-------------| | Audio on | `speaker.wave.2.fill` | `broadcast.mute.cd` | | Audio off | `speaker.slash.fill` | `broadcast.unmute.cd` | Bottone `highlighted` quando mutato (stesso verde della pausa). Pannello telemetria: riga `broadcast.audio.muted.label` se mutato. --- ## Chiavi L10n (già aggiunte, 5 lingue) | Android | iOS | |---------|-----| | `broadcast_mute_cd` | `broadcast.mute.cd` | | `broadcast_unmute_cd` | `broadcast.unmute.cd` | | `broadcast_audio_muted_label` | `broadcast.audio.muted.label` | | `broadcast_snackbar_muted` | `broadcast.snackbar.muted` | | `broadcast_snackbar_unmuted` | `broadcast.snackbar.unmuted` | | `broadcast_snackbar_muted_remote` | `broadcast.snackbar.muted.remote` | | `broadcast_snackbar_unmuted_remote` | `broadcast.snackbar.unmuted.remote` | | `broadcast_snackbar_mute_error` | `broadcast.snackbar.mute.error` | --- ## Test su Mac (acceptance) 1. `cd native/ios && python3 generate_xcodeproj.py` 2. Build simulatore / device. 3. Avvia una diretta (anche verso ambiente di staging). 4. Overlay: tap mute → icona speaker.slash, snackbar «Audio silenziato», telemetria «Audio off». 5. Apri la **regia** sullo stesso match → il pulsante deve dire «Riattiva audio». 6. Dalla regia riattiva → il telefono mostra speaker.wave e snackbar «dalla regia». 7. Mute, metti in **pausa**, riprendi: l’audio resta mutato (stato persistito). 8. YouTube / player HLS: video continua, audio silenzioso (non assente). 9. Unit test: `LiveBroadcastCoordinatorTests.testAudioMuteCommandFromCable`. ```bash cd native/ios python3 generate_xcodeproj.py xcodebuild -project MatchLiveTv.xcodeproj -scheme MatchLiveTv \ -destination 'generic/platform=iOS Simulator' \ -derivedDataPath build/DerivedData \ ONLY_ACTIVE_ARCH=YES ARCHS=arm64 EXCLUDED_ARCHS=x86_64 \ build ``` --- ## Criterio “fatto” - [ ] Build iOS senza errori HaishinKit - [ ] Mute/unmute locale in overlay - [ ] Sync bidirezionale con la regia via Cable + REST - [ ] Mute sopravvive a pausa/ripresa - [ ] AAC silenzioso in onda (niente drop della traccia audio) - [ ] Stringhe IT/EN/FR/DE/ES visibili in overlay