150 lines
6.0 KiB
Markdown
150 lines
6.0 KiB
Markdown
# 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
|