fix(tvos): make EAC3 playback conform to Dolby's guidance

Groundwork for #1300. Establishes the session, buffering and route
handling Dolby's application guide prescribes, and adds the diagnostic
arm needed to find out whether Apple's sample-buffer renderer can carry
Atmos objects at all.

Audio session, per the guide's sequence:

- Adopt the long-form playback profile in one atomic call at app launch
  and activate the session there. The SDK only accepts that policy with
  category Playback, a Default/MoviePlayback/SpokenAudio mode and no
  options, so it cannot be assembled from separate calls.
- Report the resolved rendering mode in the player, hidden unless the
  system resolves it. Apple only resolves it for CarPlay and AirPlay, so
  an unresolved value means unknown, never "not Dolby".

Diagnostics (Apple TV only, Settings > Video Playback > Atmos Output Test):

- Add a sample-buffer arm. It reads the asset with AVAssetReader at
  outputSettings nil and hands the untouched compressed buffers and the
  untouched format description straight to the renderer, with a variant
  that rebuilds the description the way playback builds it. Every
  existing mode went through AVPlayer, so nothing exercised the path
  playback actually uses; this is what tells us whether the renderer or
  our construction is at fault.
- Add an AirPlay route picker. AirPlay is the only route where the system
  resolves the rendering mode and the supported channel layouts, so it is
  what makes those observations reachable at all, and the AVPlayer arms
  now allow external playback so every arm can be compared on the same
  destination.
- Add a session-mode toggle for the one profile difference between the
  guide and previous playback behaviour.
- Report the session profile, supported layouts, both format
  descriptions, the magic cookie and the renderer status, and release the
  session on stop so a failed run cannot contaminate the next one.

Also bumps MPVKit to 1.0.14, which carries the matching audio output
work: the channel layout AVFoundation itself uses for Dolby content, a
renderer-failure observer so the fallback to PCM can actually run, the
prescribed feed ordering and preroll, flush recovery that re-supplies the
discarded audio instead of shifting later audio into its place, and
capability-driven fallback on route and capability changes.

This does not yet fix #1300. Whether the sample-buffer renderer can carry
JOC is still unknown; it removes every difference from the documented
setup that could explain the failure, and gives us the arm to answer it
on real hardware.
This commit is contained in:
edde746
2026-07-26 20:42:58 +02:00
parent a56b9a3dfb
commit 6c14049e95
62 changed files with 1743 additions and 148 deletions
+32 -4
View File
@@ -418,6 +418,15 @@ class _Translations$settings$it extends Translations$settings$en {
@override String get atmosTestRawStreamDescription => 'Trasmette il file di prova esattamente come la riproduzione Atmos del lettore. Richiede l\'URL del file di prova.';
@override String get atmosTestRawFile => 'File EAC3 grezzo';
@override String get atmosTestRawFileDescription => 'Riproduce il file di prova con lunghezza nota. Richiede l\'URL del file di prova.';
@override String get atmosTestAsbarNative => 'Renderer con buffer di campioni (nativo)';
@override String get atmosTestAsbarNativeDescription => 'Invia l\'audio compresso intatto del file direttamente al renderer di sistema. Richiede l\'URL del file di test.';
@override String get atmosTestAsbarGenerated => 'Renderer con buffer di campioni (ricostruito)';
@override String get atmosTestAsbarGeneratedDescription => 'Come sopra, ma con la descrizione audio costruita come nella riproduzione. Richiede l\'URL del file di test.';
@override String get atmosTestSessionMode => 'Usa la modalità riproduzione film';
@override String get atmosTestSessionModeDescription => 'Disattivato usa la modalità documentata da Dolby. Attivato usa la modalità precedente.';
@override String get atmosTestShowRoutePicker => 'Scegli uscita AirPlay';
@override String get atmosTestHideRoutePicker => 'Nascondi selettore uscita AirPlay';
@override String get atmosTestRoutePickerDescription => 'Invia il test a un ricevitore AirPlay. Solo AirPlay riporta la modalità audio risolta.';
@override String get atmosTestStop => 'Interrompi test';
@override String get atmosTestUrl => 'URL del file di prova';
@override String get atmosTestUrlDescription => 'URL HTTP di un file .ec3 Dolby Atmos grezzo (ad es. estratto con ffmpeg)';
@@ -1514,6 +1523,11 @@ class _Translations$videoSettings$it extends Translations$videoSettings$en {
@override String get audioOutput => 'Uscita audio';
@override String get performanceOverlay => 'Overlay prestazioni';
@override String get audioPassthrough => 'Passthrough audio';
@override String get audioOutputDolbyAtmos => 'Dolby Atmos';
@override String get audioOutputDolbyAudio => 'Dolby Audio';
@override String get audioOutputSurround => 'Surround';
@override String get audioOutputSpatial => 'Audio spaziale';
@override String get audioOutputStereo => 'Stereo';
@override String get audioNormalization => 'Normalizza il volume';
@override String get audioDownmix => 'Downmix in stereo';
}
@@ -2463,6 +2477,15 @@ extension on TranslationsIt {
'settings.atmosTestRawStreamDescription' => 'Trasmette il file di prova esattamente come la riproduzione Atmos del lettore. Richiede l\'URL del file di prova.',
'settings.atmosTestRawFile' => 'File EAC3 grezzo',
'settings.atmosTestRawFileDescription' => 'Riproduce il file di prova con lunghezza nota. Richiede l\'URL del file di prova.',
'settings.atmosTestAsbarNative' => 'Renderer con buffer di campioni (nativo)',
'settings.atmosTestAsbarNativeDescription' => 'Invia l\'audio compresso intatto del file direttamente al renderer di sistema. Richiede l\'URL del file di test.',
'settings.atmosTestAsbarGenerated' => 'Renderer con buffer di campioni (ricostruito)',
'settings.atmosTestAsbarGeneratedDescription' => 'Come sopra, ma con la descrizione audio costruita come nella riproduzione. Richiede l\'URL del file di test.',
'settings.atmosTestSessionMode' => 'Usa la modalità riproduzione film',
'settings.atmosTestSessionModeDescription' => 'Disattivato usa la modalità documentata da Dolby. Attivato usa la modalità precedente.',
'settings.atmosTestShowRoutePicker' => 'Scegli uscita AirPlay',
'settings.atmosTestHideRoutePicker' => 'Nascondi selettore uscita AirPlay',
'settings.atmosTestRoutePickerDescription' => 'Invia il test a un ricevitore AirPlay. Solo AirPlay riporta la modalità audio risolta.',
'settings.atmosTestStop' => 'Interrompi test',
'settings.atmosTestUrl' => 'URL del file di prova',
'settings.atmosTestUrlDescription' => 'URL HTTP di un file .ec3 Dolby Atmos grezzo (ad es. estratto con ffmpeg)',
@@ -2690,6 +2713,8 @@ extension on TranslationsIt {
'videoControls.language' => 'Lingua',
'videoControls.noSubtitlesFound' => 'Nessun sottotitolo trovato',
'videoControls.noSubtitlesAvailable' => 'Nessun sottotitolo disponibile',
_ => null,
} ?? switch (path) {
'videoControls.noAudioTracksAvailable' => 'Nessuna traccia audio disponibile',
'videoControls.noTracksAvailable' => 'Nessuna traccia disponibile',
'videoControls.subtitleDownloaded' => 'Sottotitolo scaricato',
@@ -2699,8 +2724,6 @@ extension on TranslationsIt {
'messages.markedAsWatched' => 'Segnato come visto',
'messages.markedAsUnwatched' => 'Segnato come non visto',
'messages.markedAsWatchedOffline' => 'Segnato come visto (verrà sincronizzato quando torni online)',
_ => null,
} ?? switch (path) {
'messages.markedAsUnwatchedOffline' => 'Segnato come non visto (verrà sincronizzato quando torni online)',
'messages.autoRemovedWatchedDownload' => ({required Object title}) => 'Rimosso automaticamente: ${title}',
'messages.autoRemovedWatchedDownloads' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('it'))(n, one: 'Rimosso automaticamente ${n} download già visto', other: 'Rimossi automaticamente ${n} download già visti', ),
@@ -3204,6 +3227,8 @@ extension on TranslationsIt {
'watchTogether.codeMustBe5Chars' => 'Il codice della sessione deve contenere 5 caratteri',
'watchTogether.joinInstructions' => 'Inserisci il codice della sessione dell\'host per partecipare.',
'watchTogether.failedToCreate' => 'Impossibile creare la sessione',
_ => null,
} ?? switch (path) {
'watchTogether.failedToJoin' => 'Impossibile unirsi alla sessione',
'watchTogether.sessionCodeCopied' => 'Codice della sessione copiato negli appunti',
'watchTogether.relayUnreachable' => 'Il server relay non è raggiungibile. Eventuali blocchi dell\'ISP potrebbero impedire l\'uso di Guarda insieme.',
@@ -3213,8 +3238,6 @@ extension on TranslationsIt {
'watchTogether.joinCurrentPlaybackDescription' => 'Torna a ciò che l\'host sta guardando in questo momento',
'watchTogether.failedToOpenCurrentPlayback' => 'Impossibile aprire la riproduzione corrente',
'watchTogether.participantJoined' => ({required Object name}) => '${name} si è unito',
_ => null,
} ?? switch (path) {
'watchTogether.participantLeft' => ({required Object name}) => '${name} se ne è andato',
'watchTogether.participantPaused' => ({required Object name}) => '${name} ha messo in pausa',
'watchTogether.participantResumed' => ({required Object name}) => '${name} ha ripreso',
@@ -3413,6 +3436,11 @@ extension on TranslationsIt {
'videoSettings.audioOutput' => 'Uscita audio',
'videoSettings.performanceOverlay' => 'Overlay prestazioni',
'videoSettings.audioPassthrough' => 'Passthrough audio',
'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos',
'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio',
'videoSettings.audioOutputSurround' => 'Surround',
'videoSettings.audioOutputSpatial' => 'Audio spaziale',
'videoSettings.audioOutputStereo' => 'Stereo',
'videoSettings.audioNormalization' => 'Normalizza il volume',
'videoSettings.audioDownmix' => 'Downmix in stereo',
'performanceOverlay.color' => 'Colore',