Merge branch 'feature/stream-audio-mute' into collaudo
Integra il mute del microfono in diretta per il collaudo. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -400,7 +400,8 @@ cd infra && cp .env.example .env && docker compose up -d --build
|
||||
| [`infrastructure/STREAMING_AUTOSCALE.md`](infrastructure/STREAMING_AUTOSCALE.md) | **Design attuale:** Proxmox prod + Hetzner Cloud warm spare (MediaMTX+ffmpeg), lab, DNS, disco; Auction in futuro |
|
||||
| [`infrastructure/HETZNER_CLOUD_RELAY_OVERFLOW.md`](infrastructure/HETZNER_CLOUD_RELAY_OVERFLOW.md) | Piano storico overflow-only relay YouTube (superseded dal doc sopra) |
|
||||
| [`ANDROID_APP_LINKS.md`](ANDROID_APP_LINKS.md) | Digital Asset Links / deep link Play (`assetlinks.json`) |
|
||||
| [`LIVE_STREAMING.md`](LIVE_STREAMING.md) | MediaMTX, pausa, HLS, ruolo ffmpeg |
|
||||
| [`LIVE_STREAMING.md`](LIVE_STREAMING.md) | MediaMTX, pausa, mute audio, HLS, ruolo ffmpeg |
|
||||
| [`IOS_STREAM_AUDIO_MUTE.md`](IOS_STREAM_AUDIO_MUTE.md) | Mute microfono iOS (allineamento Mac) |
|
||||
| [`REPLAY_MODULE.md`](REPLAY_MODULE.md) | Garage, retention, YouTube VOD |
|
||||
| [`OPS_MONITORING.md`](OPS_MONITORING.md) | ntfy, variabili ops, troubleshooting |
|
||||
| [`TABELLONI_E_OVERLAY.md`](TABELLONI_E_OVERLAY.md) | Tabelloni e overlay (app nativa) |
|
||||
@@ -417,7 +418,7 @@ cd infra && cp .env.example .env && docker compose up -d --build
|
||||
| 2026-06 | Replay: redirect 302 a `/media/` → Garage (fuori da Puma) |
|
||||
| 2026-06 | HLS live: edge → MediaMTX (fuori da Puma) |
|
||||
| 2026-06 | Ops: check `http_rails` + `http_public` separati, latenza p95 `UpLatencyTracker` |
|
||||
| 2026-06 | `RAILS_MAX_THREADS=5`; relay YouTube da MediaMTX (non via Puma) |
|
||||
| 2026-08 | Mute microfono in diretta (`audio_muted`) per evitare claim YouTube sulla musica in palestra |
|
||||
| 2026-06 | Cleanup path MediaMTX orfani (`Mediamtx::CleanupOrphanPaths`); delete path sempre a fine diretta |
|
||||
| 2026-07 | Rimosso overlay server (`OverlayRelay`); tabellone bruciato in app; HLS su path camera (non `*_air`); `YoutubeRelay` = copy video + AAC |
|
||||
| 2026-08 | `YoutubeRelay` = remux copy video+audio (niente ricodifica AAC); profilo app/slate AAC 48k mono |
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
Documento operativo per continuare su **Mac** lo sviluppo iOS e rilasciare un’app **allineata ad Android**.
|
||||
|
||||
**Aggiornato:** 8 agosto 2026
|
||||
**Aggiornato:** 12 agosto 2026
|
||||
**Riferimento Android (produzione / telefono / Play):** `2.0.10-native` (`versionCode` **31**), API `https://www.matchlivetv.it`
|
||||
**Stato iOS attuale:** marketing `2.0.10`, build `31` — i18n **allineato** (Login, hub, sheet/dialog, wizard, broadcast, account/forgot)
|
||||
|
||||
**Nuovo (branch `feature/stream-audio-mute`):** mute microfono in diretta. Codice iOS già nel branch; verifica su Mac in [`IOS_STREAM_AUDIO_MUTE.md`](IOS_STREAM_AUDIO_MUTE.md).
|
||||
|
||||
Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog, wizard, broadcast, account) + bump versione, **senza** i bug di sessione/crash già risolti su Android.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# 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
|
||||
+14
-1
@@ -1,6 +1,6 @@
|
||||
# Diretta live — architettura MediaMTX
|
||||
|
||||
**Ultimo aggiornamento:** 2026-07-29
|
||||
**Ultimo aggiornamento:** 2026-08-12
|
||||
|
||||
## Idea
|
||||
|
||||
@@ -106,6 +106,19 @@ L’app mobile persiste il punteggio con `PATCH /api/v1/sessions/:id/score` (o `
|
||||
|
||||
`GET /live/:id/status.json` resta per messaggi sotto il player e recovery HLS; `Cache-Control: no-store`.
|
||||
|
||||
### Mute audio (copyright palestra / YouTube)
|
||||
|
||||
Se in palestra c’è musica di sottofondo, YouTube può reclamare il copyright sul flusso inoltrato (`-c:a copy`). L’operatore può **silenziare il microfono** senza interrompere video, overlay, HLS o relay:
|
||||
|
||||
1. Lo stato `audio_muted` è persistito su `stream_sessions` (default `false`).
|
||||
2. API `PATCH /api/v1/sessions/:id/audio_mute` `{ "muted": true|false }` e `POST /regia/:token/audio_mute`.
|
||||
3. Action Cable: `{ type: "command", action: "mute_audio"|"unmute_audio" }` e `{ type: "stream_event", event: "audio_muted", muted: true|false }`.
|
||||
4. L’app **non toglie la traccia AAC**: azzera i sample PCM (silenzio). YouTube e MediaMTX continuano a ricevere audio 48 kHz mono.
|
||||
|
||||
Pulsante mute: overlay diretta (Android/iOS, accanto a pausa) e pagina regia.
|
||||
|
||||
Dettaglio implementazione iOS: [`IOS_STREAM_AUDIO_MUTE.md`](IOS_STREAM_AUDIO_MUTE.md).
|
||||
|
||||
## Rigenerare la slate
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user