From f93952ba6fe277b9e29e813da25ebc04c7a6783b Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:34:18 +0200 Subject: [PATCH] fix(android): stop tunneling 24p video on the Fire TV Stick 4K Tunneled playback on an AFTMM judders continuously through 23.976p direct play. The #1802 reporter isolated it: turning off Tunneled Playback with every other setting unchanged makes it smooth, and their log shows tunneling active for the whole session with E-AC3 bitstreamed and the decoded-PCM guard never firing. Audio Passthrough looked like the trigger only because it is the one user-facing switch that decides it. Passthrough off, or Downmix to Stereo on, both force the Dolby track to decode to PCM, which trips the #1458 guard and takes tunneling down with it. Passthrough on with downmix off is the only combination that keeps a bitstreamed track, so it is the only one that stays tunneled. Withdraw tunneling on that model for content at or below 30fps. The cut-off keeps 4K50/60 tunneled, which is the workload Amazon documents the feature for. The mechanism stays unconfirmed: tunneling fires no VideoFrameMetadataListener and stops media3 counting frames in the codec, so nothing app-side can measure the cadence. Only the trigger is established, and the quirk is scoped to it. That needs a frame rate the app did not have. Neither MatroskaExtractor nor Mp4Extractor populates Format.frameRate, and a tunneled session renders no frames back for the native detector, so the server's rate now rides on the open call. It is sent only for direct play, matching _primeDisplayCriteria: a transcode's metadata describes the source, not what the server is about to send. Also move Audio Passthrough out of the in-player settings sheet. It configures the audio output route rather than the current playback, and applying it mid-stream bounces the audio renderer and re-decides tunneling. Settings > Video Playback already owns it, next to Tunneled Playback, which is applied the same way. That description now mentions stutter, not only black HDR video, so the workaround is findable on hardware this quirk does not cover. The mpv backend failing to start the same 4K file is a separate defect and is not addressed here; its uploaded log is no longer retrievable. --- .../edde746/plezy/exoplayer/ExoPlayerCore.kt | 21 ++++- .../plezy/exoplayer/ExoPlayerPlugin.kt | 9 +- .../com/edde746/plezy/shared/DeviceQuirks.kt | 37 ++++++++ .../edde746/plezy/shared/DeviceQuirksTest.kt | 58 ++++++++++++ lib/i18n/az.i18n.json | 1 - lib/i18n/bg.i18n.json | 1 - lib/i18n/da.i18n.json | 1 - lib/i18n/de.i18n.json | 1 - lib/i18n/en.i18n.json | 3 +- lib/i18n/es.i18n.json | 1 - lib/i18n/fr.i18n.json | 1 - lib/i18n/hu.i18n.json | 1 - lib/i18n/it.i18n.json | 1 - lib/i18n/ja.i18n.json | 1 - lib/i18n/kk.i18n.json | 1 - lib/i18n/ko.i18n.json | 1 - lib/i18n/nb.i18n.json | 1 - lib/i18n/nl.i18n.json | 1 - lib/i18n/pl.i18n.json | 1 - lib/i18n/pt.i18n.json | 1 - lib/i18n/ru.i18n.json | 1 - lib/i18n/strings.g.dart | 2 +- lib/i18n/strings_az.g.dart | 2 - lib/i18n/strings_bg.g.dart | 2 - lib/i18n/strings_da.g.dart | 2 - lib/i18n/strings_de.g.dart | 2 - lib/i18n/strings_en.g.dart | 12 +-- lib/i18n/strings_es.g.dart | 2 - lib/i18n/strings_fr.g.dart | 2 - lib/i18n/strings_hu.g.dart | 2 - lib/i18n/strings_it.g.dart | 2 - lib/i18n/strings_ja.g.dart | 2 - lib/i18n/strings_kk.g.dart | 2 - lib/i18n/strings_ko.g.dart | 2 - lib/i18n/strings_nb.g.dart | 2 - lib/i18n/strings_nl.g.dart | 2 - lib/i18n/strings_pl.g.dart | 2 - lib/i18n/strings_pt.g.dart | 2 - lib/i18n/strings_ru.g.dart | 2 - lib/i18n/strings_sv.g.dart | 2 - lib/i18n/strings_tr.g.dart | 2 - lib/i18n/strings_uz.g.dart | 2 - lib/i18n/strings_zh.g.dart | 2 - lib/i18n/strings_zh_Hant.g.dart | 2 - lib/i18n/sv.i18n.json | 1 - lib/i18n/tr.i18n.json | 1 - lib/i18n/uz.i18n.json | 1 - lib/i18n/zh-Hant.i18n.json | 1 - lib/i18n/zh.i18n.json | 1 - lib/mpv/player/platform/player_android.dart | 11 +++ .../parts/episode_navigation.dart | 1 + .../video_player/parts/playback_open.dart | 15 +++ .../video_player/parts/playback_start.dart | 1 + .../sheets/video_settings_sheet.dart | 12 +-- ...layer_android_content_frame_rate_test.dart | 93 +++++++++++++++++++ test/widgets/video_settings_sheet_test.dart | 13 ++- 56 files changed, 263 insertions(+), 88 deletions(-) create mode 100644 android/app/src/test/kotlin/com/edde746/plezy/shared/DeviceQuirksTest.kt create mode 100644 test/mpv/player_android_content_frame_rate_test.dart 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 {