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.
This commit is contained in:
@@ -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<Map<String, Any?>>? = null
|
||||
externalSubtitleList: List<Map<String, Any?>>? = 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)"
|
||||
|
||||
@@ -64,6 +64,7 @@ class ExoPlayerPlugin :
|
||||
val autoPlay: Boolean,
|
||||
val isLive: Boolean,
|
||||
val externalSubtitles: List<Map<String, Any?>>?,
|
||||
val contentFrameRate: Float,
|
||||
private val result: MethodChannel.Result?
|
||||
) {
|
||||
private val completed = AtomicBoolean(false)
|
||||
@@ -379,6 +380,8 @@ class ExoPlayerPlugin :
|
||||
val autoPlay = call.argument<Boolean>("autoPlay") ?: true
|
||||
val isLive = call.argument<Boolean>("isLive") ?: false
|
||||
val externalSubtitles = call.argument<List<Map<String, Any?>>>("externalSubtitles")
|
||||
// Server-reported frame rate for this item; -1 when the metadata did not carry one.
|
||||
val contentFrameRate = call.argument<Number>("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
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -1650,7 +1650,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Аудио изход",
|
||||
"performanceOverlay": "Оверлей за производителност",
|
||||
"audioPassthrough": "Директно предаване на аудио",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "Съраунд",
|
||||
|
||||
@@ -1650,7 +1650,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Lydoutput",
|
||||
"performanceOverlay": "Ydelsesoverlay",
|
||||
"audioPassthrough": "Lyd-passthrough",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "Surround",
|
||||
|
||||
@@ -1650,7 +1650,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Audioausgabe",
|
||||
"performanceOverlay": "Leistungsanzeige",
|
||||
"audioPassthrough": "Audio-Durchleitung",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "Surround",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ú",
|
||||
|
||||
@@ -1650,7 +1650,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Uscita audio",
|
||||
"performanceOverlay": "Overlay prestazioni",
|
||||
"audioPassthrough": "Passthrough audio",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "Surround",
|
||||
|
||||
@@ -1641,7 +1641,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "音声出力",
|
||||
"performanceOverlay": "パフォーマンスオーバーレイ",
|
||||
"audioPassthrough": "オーディオパススルー",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "サラウンド",
|
||||
|
||||
@@ -1650,7 +1650,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Аудио шығысы",
|
||||
"performanceOverlay": "Өнімділік панелі",
|
||||
"audioPassthrough": "Дыбысты тікелей өткізу",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "Көлемді дыбыс",
|
||||
|
||||
@@ -1641,7 +1641,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "오디오 출력",
|
||||
"performanceOverlay": "성능 오버레이",
|
||||
"audioPassthrough": "오디오 패스스루",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "서라운드",
|
||||
|
||||
@@ -1650,7 +1650,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Lydutgang",
|
||||
"performanceOverlay": "Ytelsesoverlegg",
|
||||
"audioPassthrough": "Direkte lydutgang",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "Surround",
|
||||
|
||||
@@ -1650,7 +1650,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Audio-uitvoer",
|
||||
"performanceOverlay": "Prestatie-overlay",
|
||||
"audioPassthrough": "Audio-doorvoer",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "Surround",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1668,7 +1668,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Аудиовыход",
|
||||
"performanceOverlay": "Оверлей производительности",
|
||||
"audioPassthrough": "Сквозной вывод аудио",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "Объёмный звук",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => 'Съраунд',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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ú',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => 'サラウンド',
|
||||
|
||||
@@ -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' => 'Көлемді дыбыс',
|
||||
|
||||
@@ -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' => '서라운드',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => 'Объёмный звук',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => '环绕声',
|
||||
|
||||
@@ -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' => '環繞聲',
|
||||
|
||||
@@ -1650,7 +1650,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "Ljudutgång",
|
||||
"performanceOverlay": "Prestandaöverlägg",
|
||||
"audioPassthrough": "Ljudgenomströmning",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "Surround",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1641,7 +1641,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "音訊輸出",
|
||||
"performanceOverlay": "效能監控",
|
||||
"audioPassthrough": "音訊直通",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "環繞聲",
|
||||
|
||||
@@ -1641,7 +1641,6 @@
|
||||
"hdr": "HDR",
|
||||
"audioOutput": "音频输出",
|
||||
"performanceOverlay": "性能监控",
|
||||
"audioPassthrough": "音频直通",
|
||||
"audioOutputDolbyAtmos": "Dolby Atmos",
|
||||
"audioOutputDolbyAudio": "Dolby Audio",
|
||||
"audioOutputSurround": "环绕声",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -182,6 +182,7 @@ extension _VideoPlayerOpenMethods on VideoPlayerScreenState {
|
||||
required SettingsService settingsService,
|
||||
required double? preKnownFps,
|
||||
required bool hasVideoUrl,
|
||||
required bool isTranscoding,
|
||||
required Future<void> 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -632,14 +632,10 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
},
|
||||
),
|
||||
|
||||
// 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
|
||||
|
||||
@@ -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<MethodCall> _captureOpen({required Future<void> 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<Object?, Object?> _args(MethodCall call) => call.arguments as Map<Object?, Object?>;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user