diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt index 7f328d52..9ba4ad7b 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt @@ -322,6 +322,12 @@ class ExoPlayerCore(private val activity: Activity) : @Volatile private var detectedFrameRate: Float = -1f private val fpsTimestamps = LongArray(FPS_SAMPLE_COUNT) + // Frame rate the media server reported for the open item, or -1 when unknown. + // Supplied per open because the extractors media3 uses for direct play (Matroska, + // MP4) never populate Format.frameRate, and the tunneled path renders no frames + // back to the app for detectedFrameRate to derive one from. + @Volatile private var contentFrameRate: Float = -1f + @Volatile private var fpsTimestampCount = 0 private var assSyncFrameCount = 0L @@ -2590,6 +2596,7 @@ class ExoPlayerCore(private val activity: Activity) : val player = exoPlayer ?: return null val audioDelayActive = (renderersFactory?.audioDelayUs?.get() ?: 0L) != 0L return tunnelingUserEnabled && + !DeviceQuirks.hasUnreliableTunneledPlayback(contentFrameRate) && (player.playbackParameters.speed == 1f) && !tunnelingDisabledForCodec && !tunnelingDisabledForAssSubtitles && @@ -3270,7 +3277,8 @@ class ExoPlayerCore(private val activity: Activity) : autoPlay: Boolean, mediaGeneration: Int, isLive: Boolean = false, - externalSubtitleList: List>? = null + externalSubtitleList: List>? = null, + contentFrameRate: Float = -1f ) { if (!isInitialized) return @@ -3283,6 +3291,7 @@ class ExoPlayerCore(private val activity: Activity) : // Reset FPS detection for new content detectedFrameRate = -1f + this.contentFrameRate = contentFrameRate fpsTimestampCount = 0 assSyncFrameCount = 0 @@ -3400,7 +3409,14 @@ class ExoPlayerCore(private val activity: Activity) : } val sourceLabel = if (isLive) "live HLS" else "media" - emitLog("info", "media", "Opened $sourceLabel: ${redactUri(uri)}, startPosition: ${startPositionMs}ms, autoPlay: $autoPlay, sessionTunneling=$currentTunneledPlayback, userTunneling=$tunnelingUserEnabled") + emitLog( + "info", + "media", + "Opened $sourceLabel: ${redactUri(uri)}, startPosition: ${startPositionMs}ms, autoPlay: $autoPlay, " + + "contentFps=${if (contentFrameRate > 0f) contentFrameRate.toString() else "unknown"}, " + + "sessionTunneling=$currentTunneledPlayback, userTunneling=$tunnelingUserEnabled, " + + "tunnelingStatus=${exoPlayer?.let(::getTunnelingStatus) ?: "n/a"}" + ) } fun setAudioDelay(seconds: Double) { @@ -4114,6 +4130,7 @@ class ExoPlayerCore(private val activity: Activity) : private fun getTunnelingStatus(player: ExoPlayer): String { if (currentTunneledPlayback) return "Active" if (!tunnelingUserEnabled) return "Disabled by user" + if (DeviceQuirks.hasUnreliableTunneledPlayback(contentFrameRate)) return "Off (unreliable on this device)" if (player.playbackParameters.speed != 1f) return "Off (speed ≠ 1×)" if (audioNormalizationEnabled) return "Off (loudness normalization)" if (tunnelingDisabledForAudioRecovery) return "Off (audio recovery)" diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt index 47777a34..5250aa72 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt @@ -64,6 +64,7 @@ class ExoPlayerPlugin : val autoPlay: Boolean, val isLive: Boolean, val externalSubtitles: List>?, + val contentFrameRate: Float, private val result: MethodChannel.Result? ) { private val completed = AtomicBoolean(false) @@ -379,6 +380,8 @@ class ExoPlayerPlugin : val autoPlay = call.argument("autoPlay") ?: true val isLive = call.argument("isLive") ?: false val externalSubtitles = call.argument>>("externalSubtitles") + // Server-reported frame rate for this item; -1 when the metadata did not carry one. + val contentFrameRate = call.argument("contentFrameRate")?.toFloat() ?: -1f if (uri == null) { result.error("INVALID_ARGS", "Missing 'uri'", null) @@ -399,6 +402,7 @@ class ExoPlayerPlugin : autoPlay = autoPlay, isLive = isLive, externalSubtitles = externalSubtitles?.map { it.toMap() }, + contentFrameRate = contentFrameRate, result = result ) terminalEventGeneration = null @@ -458,7 +462,8 @@ class ExoPlayerPlugin : autoPlay = autoPlay, mediaGeneration = request.mediaGeneration, isLive = isLive, - externalSubtitleList = request.externalSubtitles + externalSubtitleList = request.externalSubtitles, + contentFrameRate = request.contentFrameRate ) request.success() } @@ -1394,6 +1399,8 @@ class ExoPlayerPlugin : autoPlay = playWhenReady, isLive = false, externalSubtitles = currentExternalSubtitles?.map { it.toMap() }, + // The mpv fallback core paces frames itself; the rate only gates ExoPlayer tunneling. + contentFrameRate = -1f, result = null ) fallbackInProgress = true diff --git a/android/app/src/main/kotlin/com/edde746/plezy/shared/DeviceQuirks.kt b/android/app/src/main/kotlin/com/edde746/plezy/shared/DeviceQuirks.kt index c808518e..537cb8cf 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/shared/DeviceQuirks.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/shared/DeviceQuirks.kt @@ -6,6 +6,43 @@ object DeviceQuirks { val isEWaste: Boolean get() = isGooglePixelDevice || isGoogleTensorDevice + /** + * Highest content frame rate for which [hasUnreliableTunneledPlayback] applies. + * + * Tunneling exists to meet the scheduling deadlines of high-frame-rate 4K, which Amazon + * documents and recommends for exactly that workload, so the cut-off keeps 4K50/60 + * tunneled. Withdrawing tunneling is not free even below it — the device pipeline saves + * CPU and power at any rate — but ~33ms per frame is well inside what media3's own + * release path handles, and a juddering picture is the worse trade. + */ + const val MAX_UNRELIABLE_TUNNELED_FPS = 30f + + /** + * Fire TV Stick 4K (AFTMM) judders continuously on 23.976p direct play while tunneled. + * The #1802 reporter A/B-tested it: tunneling off, every other setting unchanged, and + * the judder goes away. Untunneled, media3's VideoFrameReleaseHelper owns frame release + * and vsync-aligns it; tunneled, the device owns it and media3 notes it "may do frame + * rate conversion". + * + * The mechanism is unconfirmed. Tunneling fires no VideoFrameMetadataListener and stops + * media3 counting frames in the codec, so nothing app-side can measure the cadence or + * drops. Only the trigger is established. + * + * Deliberately scoped to the one model and the one rate class that were tested: AFTMM + * keeps tunneling for high-frame-rate content, and an unknown rate changes nothing. + * Widen only on further reports. + */ + fun hasUnreliableTunneledPlayback(contentFrameRate: Float): Boolean = hasUnreliableTunneledPlayback(Build.MANUFACTURER, Build.MODEL, contentFrameRate) + + internal fun hasUnreliableTunneledPlayback( + manufacturer: String, + model: String, + contentFrameRate: Float + ): Boolean = manufacturer.equals("Amazon", ignoreCase = true) && + model.equals("AFTMM", ignoreCase = true) && + contentFrameRate > 0f && + contentFrameRate <= MAX_UNRELIABLE_TUNNELED_FPS + private val isGooglePixelDevice: Boolean get() = ( Build.MANUFACTURER.equals("Google", ignoreCase = true) || diff --git a/android/app/src/test/kotlin/com/edde746/plezy/shared/DeviceQuirksTest.kt b/android/app/src/test/kotlin/com/edde746/plezy/shared/DeviceQuirksTest.kt new file mode 100644 index 00000000..401a5b54 --- /dev/null +++ b/android/app/src/test/kotlin/com/edde746/plezy/shared/DeviceQuirksTest.kt @@ -0,0 +1,58 @@ +package com.edde746.plezy.shared + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class DeviceQuirksTest { + + private fun unreliable( + manufacturer: String = "Amazon", + model: String = "AFTMM", + fps: Float = 23.976f + ) = DeviceQuirks.hasUnreliableTunneledPlayback(manufacturer, model, fps) + + @Test + fun fireTvStick4kJuddersOnFilmRateSoTunnelingIsWithdrawn() { + assertTrue(unreliable(fps = 23.976f)) + assertTrue(unreliable(fps = 24f)) + assertTrue(unreliable(fps = 25f)) + assertTrue(unreliable(fps = 29.97f)) + assertTrue(unreliable(fps = DeviceQuirks.MAX_UNRELIABLE_TUNNELED_FPS)) + } + + @Test + fun highFrameRateKeepsTunnelingOnTheSameDevice() { + // Tunneling exists for this workload and Amazon recommends it here, so the + // quirk must not reach 4K50/60 content. + assertFalse(unreliable(fps = 50f)) + assertFalse(unreliable(fps = 59.94f)) + assertFalse(unreliable(fps = 60f)) + } + + @Test + fun unknownFrameRateLeavesBehaviourUnchanged() { + assertFalse(unreliable(fps = -1f)) + assertFalse(unreliable(fps = 0f)) + } + + @Test + fun otherAmazonHardwareIsUnaffected() { + // Only AFTMM was reported and A/B-tested; siblings keep stock behaviour. + assertFalse(unreliable(model = "AFTKA")) + assertFalse(unreliable(model = "AFTMM2")) + assertFalse(unreliable(model = "AFTSSS")) + assertFalse(unreliable(model = "KFMAWI")) + } + + @Test + fun otherManufacturersAreUnaffected() { + assertFalse(unreliable(manufacturer = "NVIDIA", model = "SHIELD Android TV")) + assertFalse(unreliable(manufacturer = "Sony", model = "BRAVIA 4K GB")) + } + + @Test + fun buildStringCasingDoesNotDefeatTheMatch() { + assertTrue(unreliable(manufacturer = "amazon", model = "aftmm")) + } +} diff --git a/lib/i18n/az.i18n.json b/lib/i18n/az.i18n.json index ff6f620e..a5594873 100644 --- a/lib/i18n/az.i18n.json +++ b/lib/i18n/az.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Səs çıxışı", "performanceOverlay": "Məhsuldarlıq paneli", - "audioPassthrough": "Səsin birbaşa ötürülməsi", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Əhatəli səs", diff --git a/lib/i18n/bg.i18n.json b/lib/i18n/bg.i18n.json index 09e00fa0..e15e9595 100644 --- a/lib/i18n/bg.i18n.json +++ b/lib/i18n/bg.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Аудио изход", "performanceOverlay": "Оверлей за производителност", - "audioPassthrough": "Директно предаване на аудио", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Съраунд", diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index 7ad346e0..f9821411 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Lydoutput", "performanceOverlay": "Ydelsesoverlay", - "audioPassthrough": "Lyd-passthrough", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Surround", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 95b32ded..3496ebd6 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Audioausgabe", "performanceOverlay": "Leistungsanzeige", - "audioPassthrough": "Audio-Durchleitung", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Surround", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 157c9a9f..f30e84e6 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -282,7 +282,7 @@ "matchDynamicRangeDescription": "Switch HDR on for HDR content, then back to SDR", "displaySwitchDelay": "Display Switch Delay", "tunneledPlayback": "Tunneled Playback", - "tunneledPlaybackDescription": "Use video tunneling. Disable if HDR playback shows black video.", + "tunneledPlaybackDescription": "Use video tunneling. Disable if HDR playback shows black video or motion stutters.", "audioPassthrough": "Audio Passthrough", "audioPassthroughDescription": "Send Dolby/DTS audio to your receiver or TV without re-encoding, preserving surround sound. Turn off if you have no sound.", "audioPassthroughDescriptionAppleTv": "Use Apple's native Dolby decoder for Dolby Digital Plus, including Atmos. DTS and TrueHD still play as multichannel PCM. Turn off if you have no sound.", @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Audio Output", "performanceOverlay": "Performance Overlay", - "audioPassthrough": "Audio Passthrough", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Surround", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 49a7737b..db9a0355 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Salida de audio", "performanceOverlay": "Indicador de rendimiento", - "audioPassthrough": "Transferencia directa de audio", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Envolvente", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index 3db1aa96..63e1906b 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Sortie audio", "performanceOverlay": "Données de performance", - "audioPassthrough": "Transmission audio directe", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Surround", diff --git a/lib/i18n/hu.i18n.json b/lib/i18n/hu.i18n.json index 76cbe2c5..38253931 100644 --- a/lib/i18n/hu.i18n.json +++ b/lib/i18n/hu.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Hangkimenet", "performanceOverlay": "Teljesítményadatok", - "audioPassthrough": "Hangtovábbítás (passthrough)", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Térhatású", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 043eca61..faceac7c 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Uscita audio", "performanceOverlay": "Overlay prestazioni", - "audioPassthrough": "Passthrough audio", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Surround", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index a8aee63d..d7d66686 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -1641,7 +1641,6 @@ "hdr": "HDR", "audioOutput": "音声出力", "performanceOverlay": "パフォーマンスオーバーレイ", - "audioPassthrough": "オーディオパススルー", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "サラウンド", diff --git a/lib/i18n/kk.i18n.json b/lib/i18n/kk.i18n.json index 59bf2898..5ddb8050 100644 --- a/lib/i18n/kk.i18n.json +++ b/lib/i18n/kk.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Аудио шығысы", "performanceOverlay": "Өнімділік панелі", - "audioPassthrough": "Дыбысты тікелей өткізу", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Көлемді дыбыс", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index d6b2a13f..03d51a3d 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -1641,7 +1641,6 @@ "hdr": "HDR", "audioOutput": "오디오 출력", "performanceOverlay": "성능 오버레이", - "audioPassthrough": "오디오 패스스루", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "서라운드", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index f57b1365..f39e19d5 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Lydutgang", "performanceOverlay": "Ytelsesoverlegg", - "audioPassthrough": "Direkte lydutgang", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Surround", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index eeb1f0fb..fec26e89 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Audio-uitvoer", "performanceOverlay": "Prestatie-overlay", - "audioPassthrough": "Audio-doorvoer", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Surround", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index eb1e737c..70e83724 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -1668,7 +1668,6 @@ "hdr": "HDR", "audioOutput": "Wyjście audio", "performanceOverlay": "Nakładka wydajności", - "audioPassthrough": "Przekazywanie dźwięku", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Przestrzenny", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index c2499d73..ddb9765d 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Saída de áudio", "performanceOverlay": "Painel de desempenho", - "audioPassthrough": "Passagem direta de áudio", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Surround", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index dd1a5304..aec7e05b 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -1668,7 +1668,6 @@ "hdr": "HDR", "audioOutput": "Аудиовыход", "performanceOverlay": "Оверлей производительности", - "audioPassthrough": "Сквозной вывод аудио", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Объёмный звук", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index aa314d60..597d9896 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 22 -/// Strings: 32759 (1489 per locale) +/// Strings: 32737 (1488 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_az.g.dart b/lib/i18n/strings_az.g.dart index 927a0865..bc0ba691 100644 --- a/lib/i18n/strings_az.g.dart +++ b/lib/i18n/strings_az.g.dart @@ -1528,7 +1528,6 @@ class _Translations$videoSettings$az extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Səs çıxışı'; @override String get performanceOverlay => 'Məhsuldarlıq paneli'; - @override String get audioPassthrough => 'Səsin birbaşa ötürülməsi'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Əhatəli səs'; @@ -3440,7 +3439,6 @@ extension on TranslationsAz { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Səs çıxışı', 'videoSettings.performanceOverlay' => 'Məhsuldarlıq paneli', - 'videoSettings.audioPassthrough' => 'Səsin birbaşa ötürülməsi', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Əhatəli səs', diff --git a/lib/i18n/strings_bg.g.dart b/lib/i18n/strings_bg.g.dart index 219c0303..f2402ca6 100644 --- a/lib/i18n/strings_bg.g.dart +++ b/lib/i18n/strings_bg.g.dart @@ -1517,7 +1517,6 @@ class _Translations$videoSettings$bg extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Аудио изход'; @override String get performanceOverlay => 'Оверлей за производителност'; - @override String get audioPassthrough => 'Директно предаване на аудио'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Съраунд'; @@ -3418,7 +3417,6 @@ extension on TranslationsBg { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Аудио изход', 'videoSettings.performanceOverlay' => 'Оверлей за производителност', - 'videoSettings.audioPassthrough' => 'Директно предаване на аудио', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Съраунд', diff --git a/lib/i18n/strings_da.g.dart b/lib/i18n/strings_da.g.dart index f34e4eaa..a0c8b226 100644 --- a/lib/i18n/strings_da.g.dart +++ b/lib/i18n/strings_da.g.dart @@ -1517,7 +1517,6 @@ class _Translations$videoSettings$da extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Lydoutput'; @override String get performanceOverlay => 'Ydelsesoverlay'; - @override String get audioPassthrough => 'Lyd-passthrough'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Surround'; @@ -3418,7 +3417,6 @@ extension on TranslationsDa { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Lydoutput', 'videoSettings.performanceOverlay' => 'Ydelsesoverlay', - 'videoSettings.audioPassthrough' => 'Lyd-passthrough', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Surround', diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index f0740b11..a8faffdc 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -1517,7 +1517,6 @@ class _Translations$videoSettings$de extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Audioausgabe'; @override String get performanceOverlay => 'Leistungsanzeige'; - @override String get audioPassthrough => 'Audio-Durchleitung'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Surround'; @@ -3418,7 +3417,6 @@ extension on TranslationsDe { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Audioausgabe', 'videoSettings.performanceOverlay' => 'Leistungsanzeige', - 'videoSettings.audioPassthrough' => 'Audio-Durchleitung', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Surround', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 646acdad..bb1c32db 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -924,8 +924,8 @@ class Translations$settings$en { /// en: 'Tunneled Playback' String get tunneledPlayback => 'Tunneled Playback'; - /// en: 'Use video tunneling. Disable if HDR playback shows black video.' - String get tunneledPlaybackDescription => 'Use video tunneling. Disable if HDR playback shows black video.'; + /// en: 'Use video tunneling. Disable if HDR playback shows black video or motion stutters.' + String get tunneledPlaybackDescription => 'Use video tunneling. Disable if HDR playback shows black video or motion stutters.'; /// en: 'Audio Passthrough' String get audioPassthrough => 'Audio Passthrough'; @@ -4133,9 +4133,6 @@ class Translations$videoSettings$en { /// en: 'Performance Overlay' String get performanceOverlay => 'Performance Overlay'; - /// en: 'Audio Passthrough' - String get audioPassthrough => 'Audio Passthrough'; - /// en: 'Dolby Atmos' String get audioOutputDolbyAtmos => 'Dolby Atmos'; @@ -6373,7 +6370,7 @@ extension on Translations { 'settings.matchDynamicRangeDescription' => 'Switch HDR on for HDR content, then back to SDR', 'settings.displaySwitchDelay' => 'Display Switch Delay', 'settings.tunneledPlayback' => 'Tunneled Playback', - 'settings.tunneledPlaybackDescription' => 'Use video tunneling. Disable if HDR playback shows black video.', + 'settings.tunneledPlaybackDescription' => 'Use video tunneling. Disable if HDR playback shows black video or motion stutters.', 'settings.audioPassthrough' => 'Audio Passthrough', 'settings.audioPassthroughDescription' => 'Send Dolby/DTS audio to your receiver or TV without re-encoding, preserving surround sound. Turn off if you have no sound.', 'settings.audioPassthroughDescriptionAppleTv' => 'Use Apple\'s native Dolby decoder for Dolby Digital Plus, including Atmos. DTS and TrueHD still play as multichannel PCM. Turn off if you have no sound.', @@ -7605,7 +7602,6 @@ extension on Translations { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Audio Output', 'videoSettings.performanceOverlay' => 'Performance Overlay', - 'videoSettings.audioPassthrough' => 'Audio Passthrough', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Surround', @@ -7643,9 +7639,9 @@ extension on Translations { 'performanceOverlay.maxCll' => 'MaxCLL', 'performanceOverlay.maxFall' => 'MaxFALL', 'performanceOverlay.cacheUsed' => 'Cache Used', + 'performanceOverlay.cacheLimit' => 'Cache Limit', _ => null, } ?? switch (path) { - 'performanceOverlay.cacheLimit' => 'Cache Limit', 'performanceOverlay.speed' => 'Speed', 'performanceOverlay.player' => 'Player', 'performanceOverlay.memory' => 'Memory', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 33f4c8b6..ec06012e 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -1517,7 +1517,6 @@ class _Translations$videoSettings$es extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Salida de audio'; @override String get performanceOverlay => 'Indicador de rendimiento'; - @override String get audioPassthrough => 'Transferencia directa de audio'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Envolvente'; @@ -3418,7 +3417,6 @@ extension on TranslationsEs { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Salida de audio', 'videoSettings.performanceOverlay' => 'Indicador de rendimiento', - 'videoSettings.audioPassthrough' => 'Transferencia directa de audio', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Envolvente', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index afe8ae2e..8e028c76 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -1517,7 +1517,6 @@ class _Translations$videoSettings$fr extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Sortie audio'; @override String get performanceOverlay => 'Données de performance'; - @override String get audioPassthrough => 'Transmission audio directe'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Surround'; @@ -3418,7 +3417,6 @@ extension on TranslationsFr { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Sortie audio', 'videoSettings.performanceOverlay' => 'Données de performance', - 'videoSettings.audioPassthrough' => 'Transmission audio directe', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Surround', diff --git a/lib/i18n/strings_hu.g.dart b/lib/i18n/strings_hu.g.dart index 7939ae45..a11fe303 100644 --- a/lib/i18n/strings_hu.g.dart +++ b/lib/i18n/strings_hu.g.dart @@ -1517,7 +1517,6 @@ class _Translations$videoSettings$hu extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Hangkimenet'; @override String get performanceOverlay => 'Teljesítményadatok'; - @override String get audioPassthrough => 'Hangtovábbítás (passthrough)'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Térhatású'; @@ -3418,7 +3417,6 @@ extension on TranslationsHu { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Hangkimenet', 'videoSettings.performanceOverlay' => 'Teljesítményadatok', - 'videoSettings.audioPassthrough' => 'Hangtovábbítás (passthrough)', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Térhatású', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 69c39e12..a0db769f 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -1517,7 +1517,6 @@ class _Translations$videoSettings$it extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @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'; @@ -3418,7 +3417,6 @@ extension on TranslationsIt { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Uscita audio', 'videoSettings.performanceOverlay' => 'Overlay prestazioni', - 'videoSettings.audioPassthrough' => 'Passthrough audio', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Surround', diff --git a/lib/i18n/strings_ja.g.dart b/lib/i18n/strings_ja.g.dart index 2bb8e8d5..4cf5b9fa 100644 --- a/lib/i18n/strings_ja.g.dart +++ b/lib/i18n/strings_ja.g.dart @@ -1514,7 +1514,6 @@ class _Translations$videoSettings$ja extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => '音声出力'; @override String get performanceOverlay => 'パフォーマンスオーバーレイ'; - @override String get audioPassthrough => 'オーディオパススルー'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'サラウンド'; @@ -3415,7 +3414,6 @@ extension on TranslationsJa { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => '音声出力', 'videoSettings.performanceOverlay' => 'パフォーマンスオーバーレイ', - 'videoSettings.audioPassthrough' => 'オーディオパススルー', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'サラウンド', diff --git a/lib/i18n/strings_kk.g.dart b/lib/i18n/strings_kk.g.dart index 3a188f9f..4aa4ca2c 100644 --- a/lib/i18n/strings_kk.g.dart +++ b/lib/i18n/strings_kk.g.dart @@ -1528,7 +1528,6 @@ class _Translations$videoSettings$kk extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Аудио шығысы'; @override String get performanceOverlay => 'Өнімділік панелі'; - @override String get audioPassthrough => 'Дыбысты тікелей өткізу'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Көлемді дыбыс'; @@ -3440,7 +3439,6 @@ extension on TranslationsKk { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Аудио шығысы', 'videoSettings.performanceOverlay' => 'Өнімділік панелі', - 'videoSettings.audioPassthrough' => 'Дыбысты тікелей өткізу', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Көлемді дыбыс', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 89e2c6f2..5789a6be 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -1514,7 +1514,6 @@ class _Translations$videoSettings$ko extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => '오디오 출력'; @override String get performanceOverlay => '성능 오버레이'; - @override String get audioPassthrough => '오디오 패스스루'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => '서라운드'; @@ -3415,7 +3414,6 @@ extension on TranslationsKo { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => '오디오 출력', 'videoSettings.performanceOverlay' => '성능 오버레이', - 'videoSettings.audioPassthrough' => '오디오 패스스루', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => '서라운드', diff --git a/lib/i18n/strings_nb.g.dart b/lib/i18n/strings_nb.g.dart index 51a09f90..d8d368a7 100644 --- a/lib/i18n/strings_nb.g.dart +++ b/lib/i18n/strings_nb.g.dart @@ -1517,7 +1517,6 @@ class _Translations$videoSettings$nb extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Lydutgang'; @override String get performanceOverlay => 'Ytelsesoverlegg'; - @override String get audioPassthrough => 'Direkte lydutgang'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Surround'; @@ -3418,7 +3417,6 @@ extension on TranslationsNb { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Lydutgang', 'videoSettings.performanceOverlay' => 'Ytelsesoverlegg', - 'videoSettings.audioPassthrough' => 'Direkte lydutgang', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Surround', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index a9c4bde0..d1734be0 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -1517,7 +1517,6 @@ class _Translations$videoSettings$nl extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Audio-uitvoer'; @override String get performanceOverlay => 'Prestatie-overlay'; - @override String get audioPassthrough => 'Audio-doorvoer'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Surround'; @@ -3418,7 +3417,6 @@ extension on TranslationsNl { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Audio-uitvoer', 'videoSettings.performanceOverlay' => 'Prestatie-overlay', - 'videoSettings.audioPassthrough' => 'Audio-doorvoer', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Surround', diff --git a/lib/i18n/strings_pl.g.dart b/lib/i18n/strings_pl.g.dart index b63d17db..87ff6efc 100644 --- a/lib/i18n/strings_pl.g.dart +++ b/lib/i18n/strings_pl.g.dart @@ -1523,7 +1523,6 @@ class _Translations$videoSettings$pl extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Wyjście audio'; @override String get performanceOverlay => 'Nakładka wydajności'; - @override String get audioPassthrough => 'Przekazywanie dźwięku'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Przestrzenny'; @@ -3424,7 +3423,6 @@ extension on TranslationsPl { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Wyjście audio', 'videoSettings.performanceOverlay' => 'Nakładka wydajności', - 'videoSettings.audioPassthrough' => 'Przekazywanie dźwięku', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Przestrzenny', diff --git a/lib/i18n/strings_pt.g.dart b/lib/i18n/strings_pt.g.dart index 455c7077..fec7e46a 100644 --- a/lib/i18n/strings_pt.g.dart +++ b/lib/i18n/strings_pt.g.dart @@ -1517,7 +1517,6 @@ class _Translations$videoSettings$pt extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Saída de áudio'; @override String get performanceOverlay => 'Painel de desempenho'; - @override String get audioPassthrough => 'Passagem direta de áudio'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Surround'; @@ -3418,7 +3417,6 @@ extension on TranslationsPt { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Saída de áudio', 'videoSettings.performanceOverlay' => 'Painel de desempenho', - 'videoSettings.audioPassthrough' => 'Passagem direta de áudio', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Surround', diff --git a/lib/i18n/strings_ru.g.dart b/lib/i18n/strings_ru.g.dart index 01b6e8de..0fc18369 100644 --- a/lib/i18n/strings_ru.g.dart +++ b/lib/i18n/strings_ru.g.dart @@ -1523,7 +1523,6 @@ class _Translations$videoSettings$ru extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Аудиовыход'; @override String get performanceOverlay => 'Оверлей производительности'; - @override String get audioPassthrough => 'Сквозной вывод аудио'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Объёмный звук'; @@ -3424,7 +3423,6 @@ extension on TranslationsRu { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Аудиовыход', 'videoSettings.performanceOverlay' => 'Оверлей производительности', - 'videoSettings.audioPassthrough' => 'Сквозной вывод аудио', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Объёмный звук', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index f195cdc0..d3ca71fd 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -1517,7 +1517,6 @@ class _Translations$videoSettings$sv extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Ljudutgång'; @override String get performanceOverlay => 'Prestandaöverlägg'; - @override String get audioPassthrough => 'Ljudgenomströmning'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Surround'; @@ -3418,7 +3417,6 @@ extension on TranslationsSv { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Ljudutgång', 'videoSettings.performanceOverlay' => 'Prestandaöverlägg', - 'videoSettings.audioPassthrough' => 'Ljudgenomströmning', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Surround', diff --git a/lib/i18n/strings_tr.g.dart b/lib/i18n/strings_tr.g.dart index dcca3d41..6328d521 100644 --- a/lib/i18n/strings_tr.g.dart +++ b/lib/i18n/strings_tr.g.dart @@ -1528,7 +1528,6 @@ class _Translations$videoSettings$tr extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Ses Çıkışı'; @override String get performanceOverlay => 'Performans Katmanı'; - @override String get audioPassthrough => 'Ses Doğrudan Geçişi'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Çevreleyen Ses'; @@ -3440,7 +3439,6 @@ extension on TranslationsTr { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Ses Çıkışı', 'videoSettings.performanceOverlay' => 'Performans Katmanı', - 'videoSettings.audioPassthrough' => 'Ses Doğrudan Geçişi', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Çevreleyen Ses', diff --git a/lib/i18n/strings_uz.g.dart b/lib/i18n/strings_uz.g.dart index c6c80941..760a1041 100644 --- a/lib/i18n/strings_uz.g.dart +++ b/lib/i18n/strings_uz.g.dart @@ -1528,7 +1528,6 @@ class _Translations$videoSettings$uz extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => 'Audio chiqishi'; @override String get performanceOverlay => 'Unumdorlik paneli'; - @override String get audioPassthrough => 'Ovozni toʻgʻridan-toʻgʻri oʻtkazish'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => 'Surround'; @@ -3440,7 +3439,6 @@ extension on TranslationsUz { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => 'Audio chiqishi', 'videoSettings.performanceOverlay' => 'Unumdorlik paneli', - 'videoSettings.audioPassthrough' => 'Ovozni toʻgʻridan-toʻgʻri oʻtkazish', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => 'Surround', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 284179e6..e1d506bc 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -1514,7 +1514,6 @@ class Translations$videoSettings$zh extends Translations$videoSettings$en { @override String get hdr => 'HDR'; @override String get audioOutput => '音频输出'; @override String get performanceOverlay => '性能监控'; - @override String get audioPassthrough => '音频直通'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => '环绕声'; @@ -3415,7 +3414,6 @@ extension on TranslationsZh { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => '音频输出', 'videoSettings.performanceOverlay' => '性能监控', - 'videoSettings.audioPassthrough' => '音频直通', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => '环绕声', diff --git a/lib/i18n/strings_zh_Hant.g.dart b/lib/i18n/strings_zh_Hant.g.dart index ee73d105..c68f1ab3 100644 --- a/lib/i18n/strings_zh_Hant.g.dart +++ b/lib/i18n/strings_zh_Hant.g.dart @@ -1515,7 +1515,6 @@ class _Translations$videoSettings$zh_Hant extends Translations$videoSettings$zh @override String get hdr => 'HDR'; @override String get audioOutput => '音訊輸出'; @override String get performanceOverlay => '效能監控'; - @override String get audioPassthrough => '音訊直通'; @override String get audioOutputDolbyAtmos => 'Dolby Atmos'; @override String get audioOutputDolbyAudio => 'Dolby Audio'; @override String get audioOutputSurround => '環繞聲'; @@ -3416,7 +3415,6 @@ extension on TranslationsZhHant { 'videoSettings.hdr' => 'HDR', 'videoSettings.audioOutput' => '音訊輸出', 'videoSettings.performanceOverlay' => '效能監控', - 'videoSettings.audioPassthrough' => '音訊直通', 'videoSettings.audioOutputDolbyAtmos' => 'Dolby Atmos', 'videoSettings.audioOutputDolbyAudio' => 'Dolby Audio', 'videoSettings.audioOutputSurround' => '環繞聲', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 7b157fb4..37ef5bb2 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Ljudutgång", "performanceOverlay": "Prestandaöverlägg", - "audioPassthrough": "Ljudgenomströmning", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Surround", diff --git a/lib/i18n/tr.i18n.json b/lib/i18n/tr.i18n.json index f15375b1..973dca61 100644 --- a/lib/i18n/tr.i18n.json +++ b/lib/i18n/tr.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Ses Çıkışı", "performanceOverlay": "Performans Katmanı", - "audioPassthrough": "Ses Doğrudan Geçişi", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Çevreleyen Ses", diff --git a/lib/i18n/uz.i18n.json b/lib/i18n/uz.i18n.json index 50862114..0a526f94 100644 --- a/lib/i18n/uz.i18n.json +++ b/lib/i18n/uz.i18n.json @@ -1650,7 +1650,6 @@ "hdr": "HDR", "audioOutput": "Audio chiqishi", "performanceOverlay": "Unumdorlik paneli", - "audioPassthrough": "Ovozni toʻgʻridan-toʻgʻri oʻtkazish", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "Surround", diff --git a/lib/i18n/zh-Hant.i18n.json b/lib/i18n/zh-Hant.i18n.json index 1dc30090..7bd15d36 100644 --- a/lib/i18n/zh-Hant.i18n.json +++ b/lib/i18n/zh-Hant.i18n.json @@ -1641,7 +1641,6 @@ "hdr": "HDR", "audioOutput": "音訊輸出", "performanceOverlay": "效能監控", - "audioPassthrough": "音訊直通", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "環繞聲", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 828a21e1..1042c32c 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -1641,7 +1641,6 @@ "hdr": "HDR", "audioOutput": "音频输出", "performanceOverlay": "性能监控", - "audioPassthrough": "音频直通", "audioOutputDolbyAtmos": "Dolby Atmos", "audioOutputDolbyAudio": "Dolby Audio", "audioOutputSurround": "环绕声", diff --git a/lib/mpv/player/platform/player_android.dart b/lib/mpv/player/platform/player_android.dart index 2ee9f8e5..c1535943 100644 --- a/lib/mpv/player/platform/player_android.dart +++ b/lib/mpv/player/platform/player_android.dart @@ -21,6 +21,12 @@ class PlayerAndroid extends PlayerBase { int _downmixCenterBoostDb = 0; bool _downmixNormalize = true; + /// Server-reported frame rate for the next item, or null when unknown. + /// + /// Rides on `open` rather than a standalone call because it is per-item and + /// must be known before the native side settles tunneling for that item. + double? _contentFrameRate; + /// The native plugin switched from ExoPlayer to its mpv fallback for this /// session. Sticky for the instance lifetime, mirroring the native flag /// (which resets only on initialize/dispose). @@ -200,6 +206,7 @@ class PlayerAndroid extends PlayerBase { 'hasStartPosition': hasStartPosition, 'autoPlay': play, 'isLive': isLive, + if (_contentFrameRate != null) 'contentFrameRate': _contentFrameRate, if (externalSubtitles != null && externalSubtitles.isNotEmpty) 'externalSubtitles': externalSubtitles .where((s) => s.uri?.isNotEmpty == true) @@ -327,6 +334,10 @@ class PlayerAndroid extends PlayerBase { case 'tunneled-playback': _tunnelingEnabled = value != 'no'; break; + case 'content-frame-rate': + final fps = double.tryParse(value); + _contentFrameRate = fps != null && fps > 0 ? fps : null; + break; case 'dv-conversion-mode': _dvConversionMode = value; await _applyWhenInitialized( diff --git a/lib/screens/video_player/parts/episode_navigation.dart b/lib/screens/video_player/parts/episode_navigation.dart index f123d234..3d00a9db 100644 --- a/lib/screens/video_player/parts/episode_navigation.dart +++ b/lib/screens/video_player/parts/episode_navigation.dart @@ -710,6 +710,7 @@ extension _VideoPlayerEpisodeNavigationMethods on VideoPlayerScreenState { preKnownWidth: displayCriteria?.width ?? 0, preKnownHeight: displayCriteria?.height ?? 0, hasVideoUrl: true, + isTranscoding: result.isTranscoding, ensureAudioFocus: () => currentPlayer.requestAudioFocus(), ); if (frameRatePlan == null || !isCurrentReload()) { diff --git a/lib/screens/video_player/parts/playback_open.dart b/lib/screens/video_player/parts/playback_open.dart index c2f66635..5b3a3207 100644 --- a/lib/screens/video_player/parts/playback_open.dart +++ b/lib/screens/video_player/parts/playback_open.dart @@ -182,6 +182,7 @@ extension _VideoPlayerOpenMethods on VideoPlayerScreenState { required SettingsService settingsService, required double? preKnownFps, required bool hasVideoUrl, + required bool isTranscoding, required Future Function() ensureAudioFocus, int preKnownWidth = 0, int preKnownHeight = 0, @@ -192,6 +193,20 @@ extension _VideoPlayerOpenMethods on VideoPlayerScreenState { // the two Android backends: mpv needs its decoder refreshed after a // display switch (pre-load path), ExoPlayer switches pre-open instead. final isAndroidMpv = currentPlayer.needsDecoderRefreshAfterDisplaySwitch; + + // Independent of matchContentFrameRate: ExoPlayer needs the rate even when the + // display never switches, because it also decides whether video tunneling is + // safe for this item. Neither the Matroska nor the MP4 extractor populates + // Format.frameRate, and a tunneled session renders no frames back for the + // native FPS detector, so metadata is the only source. + // + // Source-side only, like _primeDisplayCriteria: a transcode's metadata rate + // describes the original file, not what the server is about to send. "0" clears + // a stale rate carried over from the previous item. + if (Platform.isAndroid && !isAndroidMpv) { + final directPlayFps = isTranscoding ? null : preKnownFps; + await currentPlayer.setProperty('content-frame-rate', (directPlayFps ?? 0).toString()); + } final needsMpvPreLoad = willAutoSwitch && isAndroidMpv && hasVideoUrl; final needsExoPreOpen = willAutoSwitch && !isAndroidMpv && hasVideoUrl; plan.needsPostOpenSwitch = willAutoSwitch && !needsMpvPreLoad && !needsExoPreOpen; diff --git a/lib/screens/video_player/parts/playback_start.dart b/lib/screens/video_player/parts/playback_start.dart index f710ce5a..b7e83f29 100644 --- a/lib/screens/video_player/parts/playback_start.dart +++ b/lib/screens/video_player/parts/playback_start.dart @@ -198,6 +198,7 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState { preKnownWidth: displayCriteria?.width ?? 0, preKnownHeight: displayCriteria?.height ?? 0, hasVideoUrl: result.videoUrl != null, + isTranscoding: result.isTranscoding, ensureAudioFocus: ensureAudioFocus, ); if (frameRatePlan == null) return; diff --git a/lib/widgets/video_controls/sheets/video_settings_sheet.dart b/lib/widgets/video_controls/sheets/video_settings_sheet.dart index 07298a77..ed8f86e3 100644 --- a/lib/widgets/video_controls/sheets/video_settings_sheet.dart +++ b/lib/widgets/video_controls/sheets/video_settings_sheet.dart @@ -632,14 +632,10 @@ class _VideoSettingsSheetState extends State { }, ), - // Audio Passthrough (desktop, Android TV, and Apple TV) - if (PlatformDetector.supportsAudioPassthrough()) - _SettingsToggleItem( - pref: SettingsService.audioPassthrough, - icon: Symbols.surround_sound_rounded, - title: t.videoSettings.audioPassthrough, - onAfterWrite: widget.player.setAudioPassthrough, - ), + // Audio Passthrough is not here: it configures the audio output route rather + // than this playback, and applying it mid-stream bounces the audio renderer and + // re-decides video tunneling. It lives in Settings > Video Playback next to + // Tunneled Playback, which is applied the same way — at the next player start. // Dolby playback badge. The Dolby application guide requires the app // to reflect AVAudioSession.renderingMode; Apple only resolves that diff --git a/test/mpv/player_android_content_frame_rate_test.dart b/test/mpv/player_android_content_frame_rate_test.dart new file mode 100644 index 00000000..75572079 --- /dev/null +++ b/test/mpv/player_android_content_frame_rate_test.dart @@ -0,0 +1,93 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/mpv/models.dart'; +import 'package:plezy/mpv/player/platform/player_android.dart'; +import 'package:plezy/services/settings_service.dart'; + +import '../test_helpers/mock_player_channels.dart'; +import '../test_helpers/prefs.dart'; + +/// Drives the content frame-rate contract between Dart and `ExoPlayerPlugin` (#1802). +/// +/// The native side gates video tunneling on this rate: a Fire TV Stick 4K judders through +/// tunneled 23.976p, so `DeviceQuirks.hasUnreliableTunneledPlayback` withdraws tunneling +/// below 30fps and leaves the 4K60 workload tunneling alone. Neither the Matroska nor the +/// MP4 extractor populates `Format.frameRate`, and a tunneled session renders no frames +/// back for the native FPS detector, so this channel argument is the only source. +/// +/// Losing it is silent — the native default is "unknown", which keeps stock behaviour and +/// simply never applies the fix. +Future _captureOpen({required Future Function(PlayerAndroid player) configure}) async { + late MethodCall open; + await withMockPlayerChannels( + methodChannelName: 'com.plezy/exo_player', + eventChannelName: 'com.plezy/exo_player/events', + methodHandler: (call) async { + if (call.method == 'open') open = call; + return call.method == 'initialize' ? true : null; + }, + testBody: () async { + final player = PlayerAndroid(); + try { + await configure(player); + await player.open(const Media('https://example.test/a.mkv'), play: false); + } finally { + await player.dispose(); + } + }, + ); + return open; +} + +Map _args(MethodCall call) => call.arguments as Map; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + resetSharedPreferencesForTest(); + SettingsService.resetForTesting(); + await SettingsService.getInstance(); + }); + + test('the metadata frame rate reaches the native open call', () async { + final open = await _captureOpen(configure: (player) => player.setProperty('content-frame-rate', '23.976')); + + expect(_args(open)['contentFrameRate'], closeTo(23.976, 1e-6)); + }); + + test('a high frame rate is forwarded unchanged so tunneling survives', () async { + // The quirk must be able to tell 4K60 apart from 24p; clamping or rounding here + // would withdraw tunneling from the workload it exists for. + final open = await _captureOpen(configure: (player) => player.setProperty('content-frame-rate', '59.94')); + + expect(_args(open)['contentFrameRate'], closeTo(59.94, 1e-6)); + }); + + test('unknown metadata sends no rate rather than a bogus one', () async { + // video_player_screen writes "0" when the server gave no frame rate. Forwarding 0 as a + // real value would be indistinguishable from a measured rate on the native side. + final open = await _captureOpen(configure: (player) => player.setProperty('content-frame-rate', '0')); + + expect(_args(open).containsKey('contentFrameRate'), isFalse); + }); + + test('an item without a rate clears the previous item\'s rate', () async { + // Episode-to-episode reuse keeps the same PlayerAndroid, so a stale 24p rate would + // keep tunneling withdrawn for a following 60fps item. + final open = await _captureOpen( + configure: (player) async { + await player.setProperty('content-frame-rate', '23.976'); + await player.setProperty('content-frame-rate', '0'); + }, + ); + + expect(_args(open).containsKey('contentFrameRate'), isFalse); + }); + + test('an unparseable rate is dropped instead of forwarded', () async { + final open = await _captureOpen(configure: (player) => player.setProperty('content-frame-rate', 'nonsense')); + + expect(_args(open).containsKey('contentFrameRate'), isFalse); + }); +} diff --git a/test/widgets/video_settings_sheet_test.dart b/test/widgets/video_settings_sheet_test.dart index 7358f700..c6bf2cb6 100644 --- a/test/widgets/video_settings_sheet_test.dart +++ b/test/widgets/video_settings_sheet_test.dart @@ -36,12 +36,19 @@ void main() { LocaleSettings.setLocaleSync(AppLocale.en); }); - testWidgets('shows audio passthrough on supported TV-style surfaces', (tester) async { + testWidgets('keeps audio passthrough out of the in-player sheet', (tester) async { + // It configures the audio output route, not this playback, and applying it + // mid-stream bounces the audio renderer and re-decides video tunneling. Settings > + // Video Playback owns it, alongside Tunneled Playback. await _pumpSheet(tester); - await tester.scrollUntilVisible(find.text('Audio Passthrough'), 500, scrollable: find.byType(Scrollable).first); + final scrollable = find.byType(Scrollable).first; + for (var i = 0; i < 10; i++) { + await tester.drag(scrollable, const Offset(0, -300)); + await tester.pumpAndSettle(); + } - expect(find.text('Audio Passthrough'), findsOneWidget); + expect(find.text('Audio Passthrough'), findsNothing); }); testWidgets('localizes Off, Normal, and Active video setting values', (tester) async {