feat(exoplayer): let the read-ahead buffer depth be chosen instead of fixed at 50s
ExoPlayer's DefaultLoadControl was built with hard-coded durations picked from one memory tier, so read-ahead stopped at 50s on any device reporting 2GB or less free, with no way to raise it. On hardware where mpv cannot render at all that ceiling is the whole buffer budget. Playback Buffer offers Auto, Large and Extra Large. The durations are taken from jellyfin-androidtv and jellyfin-android so the same words mean the same thing across Jellyfin clients; Auto keeps the memory-tiered values that shipped. Named tiers rather than a duration because a duration would be a promise the load control cannot keep: prioritizeTimeOverSizeThresholds is disabled, so targetBufferBytes stops the loader even below minBufferMs and the byte cap binds first above roughly 23 Mbit/s. The tier crosses the method channel as a string and resolves in the core, where an unrecognised name falls back to Auto. LoadControlPolicy clamps the resulting pair: media3 validates the ordering with Guava Preconditions, an unconditional throw R8 does not elide, so a bad pair would be an IllegalArgumentException out of player construction rather than a bad buffer. The two play-start thresholds stay fixed even though the Jellyfin tiers move them. BufferingStallPolicy.MIN_BUFFER_AHEAD_MS is a const derived from BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, so a runtime value there would make the stall watchdog indict a player that is obeying its own load control. That leaves the byte target as a second, often smaller ceiling, and nothing surfaced either. The resolved values now reach getStats, and the overlay's Buffer section gains a Cache Limit row reading "120s / 128MB" next to the buffered-ahead duration, so a tier that appears to do nothing on a high-bitrate file explains itself. close #1816
This commit is contained in:
@@ -289,6 +289,15 @@ class ExoPlayerCore(private val activity: Activity) :
|
|||||||
*/
|
*/
|
||||||
private var observingLoadControl: ObservingLoadControl? = null
|
private var observingLoadControl: ObservingLoadControl? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The read-ahead limits this session actually resolved to, kept so [getStats] can report them.
|
||||||
|
* Both ceilings matter and the smaller one binds: with `prioritizeTimeOverSizeThresholds`
|
||||||
|
* disabled the byte target stops the loader even below `minBufferMs`, so a raised Maximum
|
||||||
|
* Buffer that changes nothing on a UHD remux is explained by these two numbers side by side.
|
||||||
|
*/
|
||||||
|
private var resolvedTargetBufferBytes: Int? = null
|
||||||
|
private var resolvedBufferDurations: LoadControlPolicy.BufferDurations? = null
|
||||||
|
|
||||||
// Decoder hang detection: tracks gap between decoder init and first rendered frame
|
// Decoder hang detection: tracks gap between decoder init and first rendered frame
|
||||||
private var decoderHangRunnable: Runnable? = null
|
private var decoderHangRunnable: Runnable? = null
|
||||||
private var decoderInitName: String? = null
|
private var decoderInitName: String? = null
|
||||||
@@ -554,7 +563,11 @@ class ExoPlayerCore(private val activity: Activity) :
|
|||||||
bufferSizeBytes: Int? = null,
|
bufferSizeBytes: Int? = null,
|
||||||
bufferSizeAuto: Boolean = false,
|
bufferSizeAuto: Boolean = false,
|
||||||
tunnelingEnabled: Boolean = true,
|
tunnelingEnabled: Boolean = true,
|
||||||
audioPassthroughEnabled: Boolean = false
|
audioPassthroughEnabled: Boolean = false,
|
||||||
|
// Read-ahead depth, as the wire name Dart sends. Kept a String because `LoadControlPolicy`
|
||||||
|
// is internal and this function is not; unrecognised names resolve to Auto, which is also
|
||||||
|
// the default (#1816).
|
||||||
|
bufferTier: String = "auto"
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
Log.d(TAG, "Already initialized")
|
Log.d(TAG, "Already initialized")
|
||||||
@@ -777,30 +790,32 @@ class ExoPlayerCore(private val activity: Activity) :
|
|||||||
LoadControlPolicy.autoTargetBufferBytes(largeHeapMB, availableMB)
|
LoadControlPolicy.autoTargetBufferBytes(largeHeapMB, availableMB)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val resolvedTier = LoadControlPolicy.BufferTier.fromWire(bufferTier)
|
||||||
|
val bufferDurations = LoadControlPolicy.bufferDurations(resolvedTier, availableMB)
|
||||||
|
resolvedTargetBufferBytes = targetBufferBytes
|
||||||
|
resolvedBufferDurations = bufferDurations
|
||||||
|
|
||||||
val loadControl = DefaultLoadControl.Builder().apply {
|
val loadControl = DefaultLoadControl.Builder().apply {
|
||||||
setTargetBufferBytes(targetBufferBytes)
|
setTargetBufferBytes(targetBufferBytes)
|
||||||
setPrioritizeTimeOverSizeThresholds(false)
|
setPrioritizeTimeOverSizeThresholds(false)
|
||||||
if (availableMB <= 2048) {
|
// Generic setter only. media3 1.9 added ...ForStreaming/...ForLocalPlayback variants and a
|
||||||
|
// latch that stops mirroring these into the local-playback fields the moment either is
|
||||||
|
// called, which would silently give file:// playback its own defaults.
|
||||||
setBufferDurationsMs(
|
setBufferDurationsMs(
|
||||||
15_000,
|
bufferDurations.minBufferMs,
|
||||||
50_000,
|
bufferDurations.maxBufferMs,
|
||||||
LoadControlPolicy.BUFFER_FOR_PLAYBACK_MS,
|
LoadControlPolicy.BUFFER_FOR_PLAYBACK_MS,
|
||||||
LoadControlPolicy.BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
LoadControlPolicy.BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
setBufferDurationsMs(
|
|
||||||
30_000,
|
|
||||||
60_000,
|
|
||||||
LoadControlPolicy.BUFFER_FOR_PLAYBACK_MS,
|
|
||||||
LoadControlPolicy.BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.build().let { ObservingLoadControl(it).also { observing -> observingLoadControl = observing } }
|
}.build().let { ObservingLoadControl(it).also { observing -> observingLoadControl = observing } }
|
||||||
emitLog(
|
emitLog(
|
||||||
"info",
|
"info",
|
||||||
"init",
|
"init",
|
||||||
"Buffer: ${targetBufferBytes / 1024 / 1024}MB limit (${if (bufferSizeAuto) "auto" else "manual"}, " +
|
"Buffer: ${targetBufferBytes / 1024 / 1024}MB limit (${if (bufferSizeAuto) "auto" else "manual"}, " +
|
||||||
"heap=${largeHeapMB}MB, available=${availableMB}MB), tunneling=$tunnelingUserEnabled, dataSource=$dataSourceLabel"
|
"heap=${largeHeapMB}MB, available=${availableMB}MB), " +
|
||||||
|
"buffer=${bufferDurations.minBufferMs / 1000}-${bufferDurations.maxBufferMs / 1000}s " +
|
||||||
|
"(${resolvedTier.name.lowercase()}), " +
|
||||||
|
"tunneling=$tunnelingUserEnabled, dataSource=$dataSourceLabel"
|
||||||
)
|
)
|
||||||
|
|
||||||
exoPlayer = ExoPlayer.Builder(activity)
|
exoPlayer = ExoPlayer.Builder(activity)
|
||||||
@@ -4132,6 +4147,10 @@ class ExoPlayerCore(private val activity: Activity) :
|
|||||||
// Buffer metrics
|
// Buffer metrics
|
||||||
"bufferedPositionMs" to player.bufferedPosition,
|
"bufferedPositionMs" to player.bufferedPosition,
|
||||||
"currentPositionMs" to player.currentPosition,
|
"currentPositionMs" to player.currentPosition,
|
||||||
|
// Both read-ahead ceilings. The smaller binds, so a Maximum Buffer that appears to do
|
||||||
|
// nothing on a high bitrate file is explained by the byte target sitting beside it.
|
||||||
|
"bufferTargetBytes" to resolvedTargetBufferBytes,
|
||||||
|
"bufferMaxMs" to resolvedBufferDurations?.maxBufferMs,
|
||||||
"totalBufferedDurationMs" to player.totalBufferedDuration,
|
"totalBufferedDurationMs" to player.totalBufferedDuration,
|
||||||
// Playback state
|
// Playback state
|
||||||
"playbackSpeed" to player.playbackParameters.speed,
|
"playbackSpeed" to player.playbackParameters.speed,
|
||||||
|
|||||||
@@ -301,6 +301,9 @@ class ExoPlayerPlugin :
|
|||||||
val audioPassthroughEnabled = call.argument<Boolean>("audioPassthroughEnabled") ?: false
|
val audioPassthroughEnabled = call.argument<Boolean>("audioPassthroughEnabled") ?: false
|
||||||
val assVideoLatencyFrames = call.argument<Int>("assVideoLatencyFrames") ?: 0
|
val assVideoLatencyFrames = call.argument<Int>("assVideoLatencyFrames") ?: 0
|
||||||
val subtitleRenderScale = call.argument<Double>("subtitleRenderScale")?.toFloat() ?: 1.0f
|
val subtitleRenderScale = call.argument<Double>("subtitleRenderScale")?.toFloat() ?: 1.0f
|
||||||
|
// ExoPlayer-only: mpv's read-ahead is owned by the mpv.conf editor, so there is no
|
||||||
|
// fallback replay for this one. Resolved in the core; unrecognised means Auto (#1816).
|
||||||
|
val bufferTier = call.argument<String>("bufferTier") ?: "auto"
|
||||||
configuredBufferSizeBytes = bufferSizeBytes
|
configuredBufferSizeBytes = bufferSizeBytes
|
||||||
// Seed the request here rather than waiting for Dart's separate setAudioPassthrough
|
// Seed the request here rather than waiting for Dart's separate setAudioPassthrough
|
||||||
// call, so a fallback raised before that arrives still derives audio-spdif correctly.
|
// call, so a fallback raised before that arrives still derives audio-spdif correctly.
|
||||||
@@ -344,7 +347,8 @@ class ExoPlayerPlugin :
|
|||||||
bufferSizeBytes = bufferSizeBytes,
|
bufferSizeBytes = bufferSizeBytes,
|
||||||
bufferSizeAuto = bufferSizeAuto,
|
bufferSizeAuto = bufferSizeAuto,
|
||||||
tunnelingEnabled = tunnelingEnabled,
|
tunnelingEnabled = tunnelingEnabled,
|
||||||
audioPassthroughEnabled = audioPassthroughEnabled
|
audioPassthroughEnabled = audioPassthroughEnabled,
|
||||||
|
bufferTier = bufferTier
|
||||||
)
|
)
|
||||||
if (!success) {
|
if (!success) {
|
||||||
if (playerCore === core) playerCore = null
|
if (playerCore === core) playerCore = null
|
||||||
|
|||||||
@@ -47,6 +47,84 @@ internal object LoadControlPolicy {
|
|||||||
*/
|
*/
|
||||||
private const val BUDGET_DIVISOR = 4
|
private const val BUDGET_DIVISOR = 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memory tier below which the shipped Auto durations stay conservative, in
|
||||||
|
* `ActivityManager.MemoryInfo.availMem` MiB.
|
||||||
|
*/
|
||||||
|
private const val LOW_MEMORY_MB = 2048
|
||||||
|
|
||||||
|
/** Auto durations, by memory tier. These are the values that shipped before #1816. */
|
||||||
|
private const val AUTO_MIN_BUFFER_LOW_MEMORY_MS = 15_000
|
||||||
|
private const val AUTO_MAX_BUFFER_LOW_MEMORY_MS = 50_000
|
||||||
|
private const val AUTO_MIN_BUFFER_MS = 30_000
|
||||||
|
private const val AUTO_MAX_BUFFER_MS = 60_000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How deep the loader may read ahead.
|
||||||
|
*
|
||||||
|
* Named tiers rather than a duration, because a duration would be a promise this cannot keep:
|
||||||
|
* `prioritizeTimeOverSizeThresholds` is disabled, so `targetBufferBytes` stops the loader even
|
||||||
|
* below `minBufferMs`, and effective read-ahead is `min(maxBufferMs, 8 * bytes / bitrate)`. On a
|
||||||
|
* high bitrate stream the byte term wins whatever is chosen here. The tiers therefore describe
|
||||||
|
* an intent — how much network variance to absorb — and the Buffer Size setting bounds the
|
||||||
|
* memory it may cost.
|
||||||
|
*
|
||||||
|
* Values match jellyfin-androidtv and jellyfin-android so a user moving between Jellyfin clients
|
||||||
|
* gets the same behaviour from the same words.
|
||||||
|
*/
|
||||||
|
enum class BufferTier(val minBufferMs: Int, val maxBufferMs: Int) {
|
||||||
|
/** Memory-tiered defaults, resolved per device in [bufferDurations]. */
|
||||||
|
AUTO(0, 0),
|
||||||
|
|
||||||
|
/** For moderate or variable connections. */
|
||||||
|
LARGE(50_000, 120_000),
|
||||||
|
|
||||||
|
/** For slow or high-latency links, where a stall costs more than the memory does. */
|
||||||
|
EXTRA_LARGE(80_000, 240_000);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Wire values are the Dart enum's `nativeValue`. An unknown string means a Dart/Kotlin
|
||||||
|
* skew, and [AUTO] is the only safe reading of "I do not know what this user asked for".
|
||||||
|
*/
|
||||||
|
fun fromWire(value: String?): BufferTier = when (value) {
|
||||||
|
"large" -> LARGE
|
||||||
|
"extra_large" -> EXTRA_LARGE
|
||||||
|
else -> AUTO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The min/max pair handed to `DefaultLoadControl.Builder.setBufferDurationsMs`. */
|
||||||
|
data class BufferDurations(val minBufferMs: Int, val maxBufferMs: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-ahead duration bounds for this session.
|
||||||
|
*
|
||||||
|
* The returned pair always satisfies the ordering media3 asserts on
|
||||||
|
* (`bufferForPlayback* <= minBufferMs <= maxBufferMs`). That is enforced here rather than
|
||||||
|
* trusted, because media3 validates with Guava `Preconditions` — an unconditional throw that
|
||||||
|
* R8 does not elide — so a bad pair is an `IllegalArgumentException` out of
|
||||||
|
* `DefaultLoadControl.Builder.build()` on a user's device, not a degraded buffer.
|
||||||
|
*
|
||||||
|
* @param availableMB `ActivityManager.MemoryInfo.availMem`. Non-positive when unknown, which
|
||||||
|
* is treated as the low-memory tier.
|
||||||
|
*/
|
||||||
|
fun bufferDurations(tier: BufferTier, availableMB: Int): BufferDurations {
|
||||||
|
val playStartFloor = maxOf(BUFFER_FOR_PLAYBACK_MS, BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
|
||||||
|
val (rawMin, rawMax) = if (tier == BufferTier.AUTO) {
|
||||||
|
if (availableMB <= LOW_MEMORY_MB) {
|
||||||
|
AUTO_MIN_BUFFER_LOW_MEMORY_MS to AUTO_MAX_BUFFER_LOW_MEMORY_MS
|
||||||
|
} else {
|
||||||
|
AUTO_MIN_BUFFER_MS to AUTO_MAX_BUFFER_MS
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tier.minBufferMs to tier.maxBufferMs
|
||||||
|
}
|
||||||
|
val minBufferMs = rawMin.coerceAtLeast(playStartFloor)
|
||||||
|
return BufferDurations(minBufferMs, rawMax.coerceAtLeast(minBufferMs))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param largeHeapMB `ActivityManager.largeMemoryClass` — the hard Java-heap ceiling for
|
* @param largeHeapMB `ActivityManager.largeMemoryClass` — the hard Java-heap ceiling for
|
||||||
* this process, which is what bounds `DefaultAllocator` (it hands out `byte[]`).
|
* this process, which is what bounds `DefaultAllocator` (it hands out `byte[]`).
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.edde746.plezy.exoplayer
|
||||||
|
|
||||||
|
import androidx.media3.exoplayer.DefaultLoadControl
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [LoadControlPolicyTest] checks that the policy's arithmetic satisfies the ordering we believe
|
||||||
|
* media3 requires. This file checks the belief, by handing every value the settings screen can
|
||||||
|
* produce to a real `DefaultLoadControl.Builder`.
|
||||||
|
*
|
||||||
|
* The distinction matters because media3 validates with Guava `Preconditions.checkArgument`, not
|
||||||
|
* `androidx.media3.common.util.Assertions` and not a JVM `assert`. It is an unconditional throw
|
||||||
|
* that survives R8 — nothing in `proguard-rules.pro` elides it — so a rejected pair is an
|
||||||
|
* `IllegalArgumentException` out of player construction on a user's device, not a degraded buffer.
|
||||||
|
* A media3 upgrade that tightens the ordering should fail here rather than there.
|
||||||
|
*/
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
class LoadControlPolicyBuildTest {
|
||||||
|
|
||||||
|
/** Both Auto memory tiers, their boundary, and the unknown-memory case. */
|
||||||
|
private val memoryTiers = intArrayOf(0, 512, 1024, 2048, 2049, 4096, 8192)
|
||||||
|
|
||||||
|
private fun build(tier: LoadControlPolicy.BufferTier, availableMB: Int): DefaultLoadControl {
|
||||||
|
val durations = LoadControlPolicy.bufferDurations(tier, availableMB)
|
||||||
|
return DefaultLoadControl.Builder()
|
||||||
|
.setTargetBufferBytes(LoadControlPolicy.MIN_TARGET_BYTES)
|
||||||
|
.setPrioritizeTimeOverSizeThresholds(false)
|
||||||
|
.setBufferDurationsMs(
|
||||||
|
durations.minBufferMs,
|
||||||
|
durations.maxBufferMs,
|
||||||
|
LoadControlPolicy.BUFFER_FOR_PLAYBACK_MS,
|
||||||
|
LoadControlPolicy.BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun everyTierBuildsOnEveryMemoryTier() {
|
||||||
|
for (tier in LoadControlPolicy.BufferTier.entries) {
|
||||||
|
for (availableMB in memoryTiers) {
|
||||||
|
// build() throws IllegalArgumentException on a rejected ordering; reaching the next
|
||||||
|
// iteration is the assertion.
|
||||||
|
build(tier, availableMB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aTierNameThisVersionDoesNotKnowStillBuilds() {
|
||||||
|
// The tier crosses a MethodChannel as a string, so the enum is not the only possible input:
|
||||||
|
// a Dart build newer than this native build can send a name that is not in `entries`.
|
||||||
|
for (wire in arrayOf(null, "", "gigantic", "EXTRA_LARGE", "0")) {
|
||||||
|
build(LoadControlPolicy.BufferTier.fromWire(wire), availableMB = 1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun theShippedAutoTiersAreUnchanged() {
|
||||||
|
// #1816 adds a setting; it must not quietly retune the default everyone already has.
|
||||||
|
assertEquals(
|
||||||
|
LoadControlPolicy.BufferDurations(15_000, 50_000),
|
||||||
|
LoadControlPolicy.bufferDurations(LoadControlPolicy.BufferTier.AUTO, availableMB = 2048)
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
LoadControlPolicy.BufferDurations(30_000, 60_000),
|
||||||
|
LoadControlPolicy.bufferDurations(LoadControlPolicy.BufferTier.AUTO, availableMB = 2049)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.edde746.plezy.exoplayer
|
|||||||
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
private const val MIB = 1024 * 1024
|
private const val MIB = 1024 * 1024
|
||||||
@@ -84,4 +85,112 @@ class LoadControlPolicyTest {
|
|||||||
assertNull(LoadControlPolicy.readAheadSeconds(64 * MIB, 0L))
|
assertNull(LoadControlPolicy.readAheadSeconds(64 * MIB, 0L))
|
||||||
assertNull(LoadControlPolicy.readAheadSeconds(64 * MIB, -1L))
|
assertNull(LoadControlPolicy.readAheadSeconds(64 * MIB, -1L))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bufferDurations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* media3 validates the ordering with Guava `Preconditions`, which is a plain throw rather than
|
||||||
|
* a JVM assert, so a bad pair takes down `DefaultLoadControl.Builder.build()` in release too.
|
||||||
|
* Every path through the policy has to satisfy it, including inputs no UI can produce.
|
||||||
|
*/
|
||||||
|
private fun assertBuildable(durations: LoadControlPolicy.BufferDurations) {
|
||||||
|
assertTrue(
|
||||||
|
"bufferForPlaybackMs (${LoadControlPolicy.BUFFER_FOR_PLAYBACK_MS}) must not exceed " +
|
||||||
|
"minBufferMs (${durations.minBufferMs})",
|
||||||
|
LoadControlPolicy.BUFFER_FOR_PLAYBACK_MS <= durations.minBufferMs
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
"bufferForPlaybackAfterRebufferMs (${LoadControlPolicy.BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS}) " +
|
||||||
|
"must not exceed minBufferMs (${durations.minBufferMs})",
|
||||||
|
LoadControlPolicy.BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS <= durations.minBufferMs
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
"minBufferMs (${durations.minBufferMs}) must not exceed maxBufferMs (${durations.maxBufferMs})",
|
||||||
|
durations.minBufferMs <= durations.maxBufferMs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autoKeepsTheConservativeTierOnALowMemoryDevice() {
|
||||||
|
assertEquals(
|
||||||
|
LoadControlPolicy.BufferDurations(15_000, 50_000),
|
||||||
|
LoadControlPolicy.bufferDurations(LoadControlPolicy.BufferTier.AUTO, availableMB = 1024)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autoKeepsTheRoomierTierWhenMemoryAllows() {
|
||||||
|
assertEquals(
|
||||||
|
LoadControlPolicy.BufferDurations(30_000, 60_000),
|
||||||
|
LoadControlPolicy.bufferDurations(LoadControlPolicy.BufferTier.AUTO, availableMB = 4096)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun unknownMemoryTakesTheConservativeTier() {
|
||||||
|
// availMem is reported as non-positive when unknown; guessing roomy there would be the
|
||||||
|
// wrong way to be wrong on the device that can least afford it.
|
||||||
|
assertEquals(
|
||||||
|
LoadControlPolicy.BufferDurations(15_000, 50_000),
|
||||||
|
LoadControlPolicy.bufferDurations(LoadControlPolicy.BufferTier.AUTO, availableMB = 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun anExplicitTierIgnoresTheMemoryTier() {
|
||||||
|
// The whole point of #1816: a low-memory box may still want a deep buffer, because the
|
||||||
|
// constraint it is working around is the network, not the heap. Auto is the only mode that
|
||||||
|
// gets to consult memory.
|
||||||
|
assertEquals(
|
||||||
|
LoadControlPolicy.bufferDurations(LoadControlPolicy.BufferTier.LARGE, availableMB = 8192),
|
||||||
|
LoadControlPolicy.bufferDurations(LoadControlPolicy.BufferTier.LARGE, availableMB = 512)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun theTiersMatchTheJellyfinClients() {
|
||||||
|
// Borrowed verbatim from jellyfin-androidtv BufferLength and jellyfin-android
|
||||||
|
// PlayerViewModel so the same words mean the same thing across Jellyfin clients.
|
||||||
|
// Changing these silently diverges Plezy from that shared vocabulary.
|
||||||
|
assertEquals(
|
||||||
|
LoadControlPolicy.BufferDurations(50_000, 120_000),
|
||||||
|
LoadControlPolicy.bufferDurations(LoadControlPolicy.BufferTier.LARGE, availableMB = 4096)
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
LoadControlPolicy.BufferDurations(80_000, 240_000),
|
||||||
|
LoadControlPolicy.bufferDurations(LoadControlPolicy.BufferTier.EXTRA_LARGE, availableMB = 4096)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun everyTierProducesABuildablePairOnEveryMemoryTier() {
|
||||||
|
// A pair media3 rejects is a crash on play, not a bad buffer.
|
||||||
|
for (tier in LoadControlPolicy.BufferTier.entries) {
|
||||||
|
for (availableMB in intArrayOf(0, 512, 1024, 2048, 4096, 8192)) {
|
||||||
|
assertBuildable(LoadControlPolicy.bufferDurations(tier, availableMB))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferTier.fromWire
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun theWireNamesMatchTheDartNativeValues() {
|
||||||
|
assertEquals(LoadControlPolicy.BufferTier.LARGE, LoadControlPolicy.BufferTier.fromWire("large"))
|
||||||
|
assertEquals(
|
||||||
|
LoadControlPolicy.BufferTier.EXTRA_LARGE,
|
||||||
|
LoadControlPolicy.BufferTier.fromWire("extra_large")
|
||||||
|
)
|
||||||
|
assertEquals(LoadControlPolicy.BufferTier.AUTO, LoadControlPolicy.BufferTier.fromWire("auto"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun anUnrecognisedOrAbsentTierFallsBackToAuto() {
|
||||||
|
// A Dart/Kotlin skew must degrade to the shipped defaults, not to a deep buffer nobody
|
||||||
|
// asked for on a device that may not afford it.
|
||||||
|
assertEquals(LoadControlPolicy.BufferTier.AUTO, LoadControlPolicy.BufferTier.fromWire(null))
|
||||||
|
assertEquals(LoadControlPolicy.BufferTier.AUTO, LoadControlPolicy.BufferTier.fromWire(""))
|
||||||
|
assertEquals(LoadControlPolicy.BufferTier.AUTO, LoadControlPolicy.BufferTier.fromWire("EXTRA_LARGE"))
|
||||||
|
assertEquals(LoadControlPolicy.BufferTier.AUTO, LoadControlPolicy.BufferTier.fromWire("extraLarge"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Tünd",
|
"darkTheme": "Tünd",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Kitabxana sıxlığı",
|
"libraryDensity": "Kitabxana sıxlığı",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Sıx",
|
"compact": "Sıx",
|
||||||
"comfortable": "Rəhat",
|
"comfortable": "Rəhat",
|
||||||
"tvCornerSpotlightBackdrop": "Künc işıqlandırma fonu",
|
"tvCornerSpotlightBackdrop": "Künc işıqlandırma fonu",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size}MB",
|
"bufferSizeMB": "${size}MB",
|
||||||
"bufferSizeAuto": "Avtomatik (Tövsiyə olunan)",
|
"bufferSizeAuto": "Avtomatik (Tövsiyə olunan)",
|
||||||
"bufferSizeWarning": "${heap}MB yaddaş əlçatandır. ${size}MB bufer oynatmaya təsir edə bilər.",
|
"bufferSizeWarning": "${heap}MB yaddaş əlçatandır. ${size}MB bufer oynatmaya təsir edə bilər.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Defolt keyfiyyət",
|
"defaultQualityTitle": "Defolt keyfiyyət",
|
||||||
"musicQualityTitle": "Musiqi keyfiyyəti",
|
"musicQualityTitle": "Musiqi keyfiyyəti",
|
||||||
"subtitleStyling": "Altyazı tənzimləmələri",
|
"subtitleStyling": "Altyazı tənzimləmələri",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Тъмна",
|
"darkTheme": "Тъмна",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Плътност на библиотеката",
|
"libraryDensity": "Плътност на библиотеката",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Компактна",
|
"compact": "Компактна",
|
||||||
"comfortable": "Удобна",
|
"comfortable": "Удобна",
|
||||||
"tvCornerSpotlightBackdrop": "Фон с акцент в ъгъла",
|
"tvCornerSpotlightBackdrop": "Фон с акцент в ъгъла",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size} MB",
|
"bufferSizeMB": "${size} MB",
|
||||||
"bufferSizeAuto": "Автоматично (препоръчително)",
|
"bufferSizeAuto": "Автоматично (препоръчително)",
|
||||||
"bufferSizeWarning": "Налична памет: ${heap} MB. Буфер от ${size} MB може да повлияе на възпроизвеждането.",
|
"bufferSizeWarning": "Налична памет: ${heap} MB. Буфер от ${size} MB може да повлияе на възпроизвеждането.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Качество по подразбиране",
|
"defaultQualityTitle": "Качество по подразбиране",
|
||||||
"musicQualityTitle": "Качество на музиката",
|
"musicQualityTitle": "Качество на музиката",
|
||||||
"subtitleStyling": "Стил на субтитрите",
|
"subtitleStyling": "Стил на субтитрите",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Mørk",
|
"darkTheme": "Mørk",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Bibliotekstæthed",
|
"libraryDensity": "Bibliotekstæthed",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Kompakt",
|
"compact": "Kompakt",
|
||||||
"comfortable": "Komfortabel",
|
"comfortable": "Komfortabel",
|
||||||
"tvCornerSpotlightBackdrop": "Fremhævet baggrundsbillede i hjørnet",
|
"tvCornerSpotlightBackdrop": "Fremhævet baggrundsbillede i hjørnet",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size}MB",
|
"bufferSizeMB": "${size}MB",
|
||||||
"bufferSizeAuto": "Automatisk (anbefalet)",
|
"bufferSizeAuto": "Automatisk (anbefalet)",
|
||||||
"bufferSizeWarning": "${heap} MB hukommelse tilgængelig. En buffer på ${size} MB kan påvirke afspilningen.",
|
"bufferSizeWarning": "${heap} MB hukommelse tilgængelig. En buffer på ${size} MB kan påvirke afspilningen.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Standardkvalitet",
|
"defaultQualityTitle": "Standardkvalitet",
|
||||||
"musicQualityTitle": "Musikkvalitet",
|
"musicQualityTitle": "Musikkvalitet",
|
||||||
"subtitleStyling": "Undertekststil",
|
"subtitleStyling": "Undertekststil",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Dunkel",
|
"darkTheme": "Dunkel",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Darstellungsdichte der Mediathek",
|
"libraryDensity": "Darstellungsdichte der Mediathek",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Kompakt",
|
"compact": "Kompakt",
|
||||||
"comfortable": "Großzügig",
|
"comfortable": "Großzügig",
|
||||||
"tvCornerSpotlightBackdrop": "Backdrop in der Ecke",
|
"tvCornerSpotlightBackdrop": "Backdrop in der Ecke",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size} MB",
|
"bufferSizeMB": "${size} MB",
|
||||||
"bufferSizeAuto": "Automatisch (empfohlen)",
|
"bufferSizeAuto": "Automatisch (empfohlen)",
|
||||||
"bufferSizeWarning": "${heap} MB Speicher verfügbar. Ein Puffer von ${size} MB kann die Wiedergabe beeinträchtigen.",
|
"bufferSizeWarning": "${heap} MB Speicher verfügbar. Ein Puffer von ${size} MB kann die Wiedergabe beeinträchtigen.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Standardqualität",
|
"defaultQualityTitle": "Standardqualität",
|
||||||
"musicQualityTitle": "Musikqualität",
|
"musicQualityTitle": "Musikqualität",
|
||||||
"subtitleStyling": "Untertitel-Stil",
|
"subtitleStyling": "Untertitel-Stil",
|
||||||
|
|||||||
@@ -170,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size}MB",
|
"bufferSizeMB": "${size}MB",
|
||||||
"bufferSizeAuto": "Auto (Recommended)",
|
"bufferSizeAuto": "Auto (Recommended)",
|
||||||
"bufferSizeWarning": "${heap}MB memory available. A ${size}MB buffer may affect playback.",
|
"bufferSizeWarning": "${heap}MB memory available. A ${size}MB buffer may affect playback.",
|
||||||
|
"playbackBuffer": "Playback Buffer",
|
||||||
|
"playbackBufferAuto": "Auto (Recommended)",
|
||||||
|
"playbackBufferLarge": "Large",
|
||||||
|
"playbackBufferExtraLarge": "Extra Large",
|
||||||
|
"playbackBufferDescription": "Buffer more against unstable connections. Also limited by Buffer Size.",
|
||||||
"defaultQualityTitle": "Default Quality",
|
"defaultQualityTitle": "Default Quality",
|
||||||
"musicQualityTitle": "Music Quality",
|
"musicQualityTitle": "Music Quality",
|
||||||
"subtitleStyling": "Subtitle Styling",
|
"subtitleStyling": "Subtitle Styling",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Oscuro",
|
"darkTheme": "Oscuro",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Densidad de la biblioteca",
|
"libraryDensity": "Densidad de la biblioteca",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Compacto",
|
"compact": "Compacto",
|
||||||
"comfortable": "Cómodo",
|
"comfortable": "Cómodo",
|
||||||
"tvCornerSpotlightBackdrop": "Imagen destacada en la esquina",
|
"tvCornerSpotlightBackdrop": "Imagen destacada en la esquina",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size} MB",
|
"bufferSizeMB": "${size} MB",
|
||||||
"bufferSizeAuto": "Automático (recomendado)",
|
"bufferSizeAuto": "Automático (recomendado)",
|
||||||
"bufferSizeWarning": "${heap} MB de memoria disponible. Un búfer de ${size} MB puede afectar a la reproducción.",
|
"bufferSizeWarning": "${heap} MB de memoria disponible. Un búfer de ${size} MB puede afectar a la reproducción.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Calidad predeterminada",
|
"defaultQualityTitle": "Calidad predeterminada",
|
||||||
"musicQualityTitle": "Calidad de música",
|
"musicQualityTitle": "Calidad de música",
|
||||||
"subtitleStyling": "Estilo de subtítulos",
|
"subtitleStyling": "Estilo de subtítulos",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Sombre",
|
"darkTheme": "Sombre",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Densité des bibliothèques",
|
"libraryDensity": "Densité des bibliothèques",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Compact",
|
"compact": "Compact",
|
||||||
"comfortable": "Confortable",
|
"comfortable": "Confortable",
|
||||||
"tvCornerSpotlightBackdrop": "Illustration en vedette dans le coin",
|
"tvCornerSpotlightBackdrop": "Illustration en vedette dans le coin",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size} Mo",
|
"bufferSizeMB": "${size} Mo",
|
||||||
"bufferSizeAuto": "Automatique (recommandé)",
|
"bufferSizeAuto": "Automatique (recommandé)",
|
||||||
"bufferSizeWarning": "${heap} Mo de mémoire disponible. Un tampon de ${size} Mo peut affecter la lecture.",
|
"bufferSizeWarning": "${heap} Mo de mémoire disponible. Un tampon de ${size} Mo peut affecter la lecture.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Qualité par défaut",
|
"defaultQualityTitle": "Qualité par défaut",
|
||||||
"musicQualityTitle": "Qualité de la musique",
|
"musicQualityTitle": "Qualité de la musique",
|
||||||
"subtitleStyling": "Style des sous-titres",
|
"subtitleStyling": "Style des sous-titres",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Sötét",
|
"darkTheme": "Sötét",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Könyvtársűrűség",
|
"libraryDensity": "Könyvtársűrűség",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Kompakt",
|
"compact": "Kompakt",
|
||||||
"comfortable": "Kényelmes",
|
"comfortable": "Kényelmes",
|
||||||
"tvCornerSpotlightBackdrop": "Sarokban megjelenő kiemelt háttérkép",
|
"tvCornerSpotlightBackdrop": "Sarokban megjelenő kiemelt háttérkép",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size} MB",
|
"bufferSizeMB": "${size} MB",
|
||||||
"bufferSizeAuto": "Automatikus (ajánlott)",
|
"bufferSizeAuto": "Automatikus (ajánlott)",
|
||||||
"bufferSizeWarning": "${heap} MB memória érhető el. A(z) ${size} MB méretű puffer befolyásolhatja a lejátszást.",
|
"bufferSizeWarning": "${heap} MB memória érhető el. A(z) ${size} MB méretű puffer befolyásolhatja a lejátszást.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Alapértelmezett minőség",
|
"defaultQualityTitle": "Alapértelmezett minőség",
|
||||||
"musicQualityTitle": "Zene minősége",
|
"musicQualityTitle": "Zene minősége",
|
||||||
"subtitleStyling": "Feliratok stílusa",
|
"subtitleStyling": "Feliratok stílusa",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Scuro",
|
"darkTheme": "Scuro",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Densità della libreria",
|
"libraryDensity": "Densità della libreria",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Compatta",
|
"compact": "Compatta",
|
||||||
"comfortable": "Comoda",
|
"comfortable": "Comoda",
|
||||||
"tvCornerSpotlightBackdrop": "Sfondo in evidenza nell'angolo",
|
"tvCornerSpotlightBackdrop": "Sfondo in evidenza nell'angolo",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size}MB",
|
"bufferSizeMB": "${size}MB",
|
||||||
"bufferSizeAuto": "Automatica (consigliata)",
|
"bufferSizeAuto": "Automatica (consigliata)",
|
||||||
"bufferSizeWarning": "${heap}MB di memoria disponibile. Un buffer di ${size}MB può influire sulla riproduzione.",
|
"bufferSizeWarning": "${heap}MB di memoria disponibile. Un buffer di ${size}MB può influire sulla riproduzione.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Qualità predefinita",
|
"defaultQualityTitle": "Qualità predefinita",
|
||||||
"musicQualityTitle": "Qualità musicale",
|
"musicQualityTitle": "Qualità musicale",
|
||||||
"subtitleStyling": "Stile sottotitoli",
|
"subtitleStyling": "Stile sottotitoli",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "ダーク",
|
"darkTheme": "ダーク",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "ライブラリの密度",
|
"libraryDensity": "ライブラリの密度",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "コンパクト",
|
"compact": "コンパクト",
|
||||||
"comfortable": "ゆったり",
|
"comfortable": "ゆったり",
|
||||||
"tvCornerSpotlightBackdrop": "画面隅の注目コンテンツ背景",
|
"tvCornerSpotlightBackdrop": "画面隅の注目コンテンツ背景",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size}MB",
|
"bufferSizeMB": "${size}MB",
|
||||||
"bufferSizeAuto": "自動(推奨)",
|
"bufferSizeAuto": "自動(推奨)",
|
||||||
"bufferSizeWarning": "${heap}MBのメモリが利用可能です。${size}MBのバッファは再生に影響する可能性があります。",
|
"bufferSizeWarning": "${heap}MBのメモリが利用可能です。${size}MBのバッファは再生に影響する可能性があります。",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "デフォルト画質",
|
"defaultQualityTitle": "デフォルト画質",
|
||||||
"musicQualityTitle": "音楽の音質",
|
"musicQualityTitle": "音楽の音質",
|
||||||
"subtitleStyling": "字幕スタイル",
|
"subtitleStyling": "字幕スタイル",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Қараңғы",
|
"darkTheme": "Қараңғы",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Кітапхана тығыздығы",
|
"libraryDensity": "Кітапхана тығыздығы",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Тығыз",
|
"compact": "Тығыз",
|
||||||
"comfortable": "Ыңғайлы",
|
"comfortable": "Ыңғайлы",
|
||||||
"tvCornerSpotlightBackdrop": "Бұрыштық жарық фоны",
|
"tvCornerSpotlightBackdrop": "Бұрыштық жарық фоны",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size} МБ",
|
"bufferSizeMB": "${size} МБ",
|
||||||
"bufferSizeAuto": "Автоматты (Ұсынылатын)",
|
"bufferSizeAuto": "Автоматты (Ұсынылатын)",
|
||||||
"bufferSizeWarning": "${heap} МБ ЖҰД қолжетімді. ${size} МБ буфер ойнатуға әсер етуі мүмкін.",
|
"bufferSizeWarning": "${heap} МБ ЖҰД қолжетімді. ${size} МБ буфер ойнатуға әсер етуі мүмкін.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Әдепкі сапа",
|
"defaultQualityTitle": "Әдепкі сапа",
|
||||||
"musicQualityTitle": "Музыка сапасы",
|
"musicQualityTitle": "Музыка сапасы",
|
||||||
"subtitleStyling": "Субтитр баптаулары",
|
"subtitleStyling": "Субтитр баптаулары",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "다크",
|
"darkTheme": "다크",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "라이브러리 표시 밀도",
|
"libraryDensity": "라이브러리 표시 밀도",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "조밀하게",
|
"compact": "조밀하게",
|
||||||
"comfortable": "여유롭게",
|
"comfortable": "여유롭게",
|
||||||
"tvCornerSpotlightBackdrop": "모서리 스포트라이트 배경",
|
"tvCornerSpotlightBackdrop": "모서리 스포트라이트 배경",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size}MB",
|
"bufferSizeMB": "${size}MB",
|
||||||
"bufferSizeAuto": "자동 (권장)",
|
"bufferSizeAuto": "자동 (권장)",
|
||||||
"bufferSizeWarning": "${heap}MB 메모리를 사용할 수 있습니다. ${size}MB 버퍼는 재생에 영향을 줄 수 있습니다.",
|
"bufferSizeWarning": "${heap}MB 메모리를 사용할 수 있습니다. ${size}MB 버퍼는 재생에 영향을 줄 수 있습니다.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "기본 화질",
|
"defaultQualityTitle": "기본 화질",
|
||||||
"musicQualityTitle": "음악 음질",
|
"musicQualityTitle": "음악 음질",
|
||||||
"subtitleStyling": "자막 스타일",
|
"subtitleStyling": "자막 스타일",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Mørkt",
|
"darkTheme": "Mørkt",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Innholdstetthet i biblioteket",
|
"libraryDensity": "Innholdstetthet i biblioteket",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Kompakt",
|
"compact": "Kompakt",
|
||||||
"comfortable": "Komfortabel",
|
"comfortable": "Komfortabel",
|
||||||
"tvCornerSpotlightBackdrop": "Fremhevet bakgrunn i hjørnet",
|
"tvCornerSpotlightBackdrop": "Fremhevet bakgrunn i hjørnet",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size} MB",
|
"bufferSizeMB": "${size} MB",
|
||||||
"bufferSizeAuto": "Automatisk (anbefalt)",
|
"bufferSizeAuto": "Automatisk (anbefalt)",
|
||||||
"bufferSizeWarning": "${heap} MB minne tilgjengelig. En buffer på ${size} MB kan påvirke avspillingen.",
|
"bufferSizeWarning": "${heap} MB minne tilgjengelig. En buffer på ${size} MB kan påvirke avspillingen.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Standardkvalitet",
|
"defaultQualityTitle": "Standardkvalitet",
|
||||||
"musicQualityTitle": "Musikkvalitet",
|
"musicQualityTitle": "Musikkvalitet",
|
||||||
"subtitleStyling": "Undertekststil",
|
"subtitleStyling": "Undertekststil",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Donker",
|
"darkTheme": "Donker",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Bibliotheekdichtheid",
|
"libraryDensity": "Bibliotheekdichtheid",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Compact",
|
"compact": "Compact",
|
||||||
"comfortable": "Comfortabel",
|
"comfortable": "Comfortabel",
|
||||||
"tvCornerSpotlightBackdrop": "Uitgelichte achtergrond in de hoek",
|
"tvCornerSpotlightBackdrop": "Uitgelichte achtergrond in de hoek",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size}MB",
|
"bufferSizeMB": "${size}MB",
|
||||||
"bufferSizeAuto": "Automatisch (aanbevolen)",
|
"bufferSizeAuto": "Automatisch (aanbevolen)",
|
||||||
"bufferSizeWarning": "${heap}MB geheugen beschikbaar. Een buffer van ${size}MB kan afspelen beïnvloeden.",
|
"bufferSizeWarning": "${heap}MB geheugen beschikbaar. Een buffer van ${size}MB kan afspelen beïnvloeden.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Standaardkwaliteit",
|
"defaultQualityTitle": "Standaardkwaliteit",
|
||||||
"musicQualityTitle": "Muziekkwaliteit",
|
"musicQualityTitle": "Muziekkwaliteit",
|
||||||
"subtitleStyling": "Ondertitelopmaak",
|
"subtitleStyling": "Ondertitelopmaak",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Ciemny",
|
"darkTheme": "Ciemny",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Gęstość biblioteki",
|
"libraryDensity": "Gęstość biblioteki",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Kompaktowy",
|
"compact": "Kompaktowy",
|
||||||
"comfortable": "Wygodny",
|
"comfortable": "Wygodny",
|
||||||
"tvCornerSpotlightBackdrop": "Tło wyróżnionej pozycji w rogu",
|
"tvCornerSpotlightBackdrop": "Tło wyróżnionej pozycji w rogu",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size}MB",
|
"bufferSizeMB": "${size}MB",
|
||||||
"bufferSizeAuto": "Automatyczny (zalecany)",
|
"bufferSizeAuto": "Automatyczny (zalecany)",
|
||||||
"bufferSizeWarning": "Dostępna pamięć: ${heap}MB. Bufor ${size}MB może wpłynąć na odtwarzanie.",
|
"bufferSizeWarning": "Dostępna pamięć: ${heap}MB. Bufor ${size}MB może wpłynąć na odtwarzanie.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Domyślna jakość",
|
"defaultQualityTitle": "Domyślna jakość",
|
||||||
"musicQualityTitle": "Jakość muzyki",
|
"musicQualityTitle": "Jakość muzyki",
|
||||||
"subtitleStyling": "Styl napisów",
|
"subtitleStyling": "Styl napisów",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Escuro",
|
"darkTheme": "Escuro",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Densidade da Biblioteca",
|
"libraryDensity": "Densidade da Biblioteca",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Compacto",
|
"compact": "Compacto",
|
||||||
"comfortable": "Confortável",
|
"comfortable": "Confortável",
|
||||||
"tvCornerSpotlightBackdrop": "Imagem de destaque no canto",
|
"tvCornerSpotlightBackdrop": "Imagem de destaque no canto",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size}MB",
|
"bufferSizeMB": "${size}MB",
|
||||||
"bufferSizeAuto": "Automático (Recomendado)",
|
"bufferSizeAuto": "Automático (Recomendado)",
|
||||||
"bufferSizeWarning": "${heap}MB de memória disponível. Um buffer de ${size}MB pode afetar a reprodução.",
|
"bufferSizeWarning": "${heap}MB de memória disponível. Um buffer de ${size}MB pode afetar a reprodução.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Qualidade padrão",
|
"defaultQualityTitle": "Qualidade padrão",
|
||||||
"musicQualityTitle": "Qualidade da música",
|
"musicQualityTitle": "Qualidade da música",
|
||||||
"subtitleStyling": "Estilo de Legendas",
|
"subtitleStyling": "Estilo de Legendas",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Тёмная",
|
"darkTheme": "Тёмная",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Плотность библиотеки",
|
"libraryDensity": "Плотность библиотеки",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Компактный",
|
"compact": "Компактный",
|
||||||
"comfortable": "Комфортный",
|
"comfortable": "Комфортный",
|
||||||
"tvCornerSpotlightBackdrop": "Фоновое изображение в углу",
|
"tvCornerSpotlightBackdrop": "Фоновое изображение в углу",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size}МБ",
|
"bufferSizeMB": "${size}МБ",
|
||||||
"bufferSizeAuto": "Авто (Рекомендуется)",
|
"bufferSizeAuto": "Авто (Рекомендуется)",
|
||||||
"bufferSizeWarning": "Доступно памяти: ${heap} МБ. Буфер размером ${size} МБ может повлиять на воспроизведение.",
|
"bufferSizeWarning": "Доступно памяти: ${heap} МБ. Буфер размером ${size} МБ может повлиять на воспроизведение.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Качество по умолчанию",
|
"defaultQualityTitle": "Качество по умолчанию",
|
||||||
"musicQualityTitle": "Качество музыки",
|
"musicQualityTitle": "Качество музыки",
|
||||||
"subtitleStyling": "Стиль субтитров",
|
"subtitleStyling": "Стиль субтитров",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 22
|
/// Locales: 22
|
||||||
/// Strings: 32738 (1488 per locale)
|
/// Strings: 32743 (1488 per locale)
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|||||||
@@ -588,6 +588,21 @@ class Translations$settings$en {
|
|||||||
/// en: '${heap}MB memory available. A ${size}MB buffer may affect playback.'
|
/// en: '${heap}MB memory available. A ${size}MB buffer may affect playback.'
|
||||||
String bufferSizeWarning({required Object heap, required Object size}) => '${heap}MB memory available. A ${size}MB buffer may affect playback.';
|
String bufferSizeWarning({required Object heap, required Object size}) => '${heap}MB memory available. A ${size}MB buffer may affect playback.';
|
||||||
|
|
||||||
|
/// en: 'Playback Buffer'
|
||||||
|
String get playbackBuffer => 'Playback Buffer';
|
||||||
|
|
||||||
|
/// en: 'Auto (Recommended)'
|
||||||
|
String get playbackBufferAuto => 'Auto (Recommended)';
|
||||||
|
|
||||||
|
/// en: 'Large'
|
||||||
|
String get playbackBufferLarge => 'Large';
|
||||||
|
|
||||||
|
/// en: 'Extra Large'
|
||||||
|
String get playbackBufferExtraLarge => 'Extra Large';
|
||||||
|
|
||||||
|
/// en: 'Buffer more against unstable connections. Also limited by Buffer Size.'
|
||||||
|
String get playbackBufferDescription => 'Buffer more against unstable connections. Also limited by Buffer Size.';
|
||||||
|
|
||||||
/// en: 'Default Quality'
|
/// en: 'Default Quality'
|
||||||
String get defaultQualityTitle => 'Default Quality';
|
String get defaultQualityTitle => 'Default Quality';
|
||||||
|
|
||||||
@@ -6261,6 +6276,11 @@ extension on Translations {
|
|||||||
'settings.bufferSizeMB' => ({required Object size}) => '${size}MB',
|
'settings.bufferSizeMB' => ({required Object size}) => '${size}MB',
|
||||||
'settings.bufferSizeAuto' => 'Auto (Recommended)',
|
'settings.bufferSizeAuto' => 'Auto (Recommended)',
|
||||||
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap}MB memory available. A ${size}MB buffer may affect playback.',
|
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap}MB memory available. A ${size}MB buffer may affect playback.',
|
||||||
|
'settings.playbackBuffer' => 'Playback Buffer',
|
||||||
|
'settings.playbackBufferAuto' => 'Auto (Recommended)',
|
||||||
|
'settings.playbackBufferLarge' => 'Large',
|
||||||
|
'settings.playbackBufferExtraLarge' => 'Extra Large',
|
||||||
|
'settings.playbackBufferDescription' => 'Buffer more against unstable connections. Also limited by Buffer Size.',
|
||||||
'settings.defaultQualityTitle' => 'Default Quality',
|
'settings.defaultQualityTitle' => 'Default Quality',
|
||||||
'settings.musicQualityTitle' => 'Music Quality',
|
'settings.musicQualityTitle' => 'Music Quality',
|
||||||
'settings.subtitleStyling' => 'Subtitle Styling',
|
'settings.subtitleStyling' => 'Subtitle Styling',
|
||||||
@@ -6610,13 +6630,13 @@ extension on Translations {
|
|||||||
'mediaMenu.deleteMovieTitle' => 'Delete this movie?',
|
'mediaMenu.deleteMovieTitle' => 'Delete this movie?',
|
||||||
'mediaMenu.deleteEpisodeConfirm' => 'Delete episode',
|
'mediaMenu.deleteEpisodeConfirm' => 'Delete episode',
|
||||||
'mediaMenu.deleteSeasonConfirm' => 'Delete season',
|
'mediaMenu.deleteSeasonConfirm' => 'Delete season',
|
||||||
|
_ => null,
|
||||||
|
} ?? switch (path) {
|
||||||
'mediaMenu.deleteShowConfirm' => 'Delete show',
|
'mediaMenu.deleteShowConfirm' => 'Delete show',
|
||||||
'mediaMenu.deleteMovieConfirm' => 'Delete movie',
|
'mediaMenu.deleteMovieConfirm' => 'Delete movie',
|
||||||
'mediaMenu.deleteAnyway' => 'Delete anyway',
|
'mediaMenu.deleteAnyway' => 'Delete anyway',
|
||||||
'mediaMenu.confirmDeleteTarget' => ({required Object title}) => 'Permanently delete ${title} from your server?',
|
'mediaMenu.confirmDeleteTarget' => ({required Object title}) => 'Permanently delete ${title} from your server?',
|
||||||
'mediaMenu.deleteMultipleWarning' => 'This includes all episodes and their files.',
|
'mediaMenu.deleteMultipleWarning' => 'This includes all episodes and their files.',
|
||||||
_ => null,
|
|
||||||
} ?? switch (path) {
|
|
||||||
'mediaMenu.deleteEpisodeCountWarning' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: 'This deletes all ${n} episode in it, and its file.', other: 'This deletes all ${n} episodes in it, and their files.', ),
|
'mediaMenu.deleteEpisodeCountWarning' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: 'This deletes all ${n} episode in it, and its file.', other: 'This deletes all ${n} episodes in it, and their files.', ),
|
||||||
'mediaMenu.deleteMultiPartWarning' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: 'This item is stored as ${n} file, which will be deleted.', other: 'This item is stored across ${n} files, and all of them will be deleted.', ),
|
'mediaMenu.deleteMultiPartWarning' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: 'This item is stored as ${n} file, which will be deleted.', other: 'This item is stored across ${n} files, and all of them will be deleted.', ),
|
||||||
'mediaMenu.deleteSharedFileHeading' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: '${n} other episode is stored in the same file and will be deleted too:', other: '${n} other episodes are stored in the same file and will be deleted too:', ),
|
'mediaMenu.deleteSharedFileHeading' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: '${n} other episode is stored in the same file and will be deleted too:', other: '${n} other episodes are stored in the same file and will be deleted too:', ),
|
||||||
@@ -7124,13 +7144,13 @@ extension on Translations {
|
|||||||
'explore.badge.pendingApproval' => 'Pending approval',
|
'explore.badge.pendingApproval' => 'Pending approval',
|
||||||
'explore.badge.processing' => 'Processing',
|
'explore.badge.processing' => 'Processing',
|
||||||
'explore.badge.declined' => 'Declined',
|
'explore.badge.declined' => 'Declined',
|
||||||
|
_ => null,
|
||||||
|
} ?? switch (path) {
|
||||||
'explore.badge.requestFailed' => 'Request failed',
|
'explore.badge.requestFailed' => 'Request failed',
|
||||||
'explore.badge.requested4k' => '4K requested',
|
'explore.badge.requested4k' => '4K requested',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} seasons',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} seasons',
|
||||||
'explore.badge.nextEpisodeIn' => ({required Object episode, required Object duration}) => 'Ep ${episode} in ${duration}',
|
'explore.badge.nextEpisodeIn' => ({required Object episode, required Object duration}) => 'Ep ${episode} in ${duration}',
|
||||||
'explore.badge.nextAiringIn' => ({required Object duration}) => 'Next in ${duration}',
|
'explore.badge.nextAiringIn' => ({required Object duration}) => 'Next in ${duration}',
|
||||||
_ => null,
|
|
||||||
} ?? switch (path) {
|
|
||||||
'explore.badge.episodesShort' => ({required Object n}) => '${n} eps',
|
'explore.badge.episodesShort' => ({required Object n}) => '${n} eps',
|
||||||
'explore.badge.minutesPerEpisode' => ({required Object n}) => '${n} min/ep',
|
'explore.badge.minutesPerEpisode' => ({required Object n}) => '${n} min/ep',
|
||||||
'explore.badge.adult' => '18+',
|
'explore.badge.adult' => '18+',
|
||||||
@@ -7638,13 +7658,13 @@ extension on Translations {
|
|||||||
'performanceOverlay.dvRpus' => 'DV RPUs',
|
'performanceOverlay.dvRpus' => 'DV RPUs',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU Avg',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU Avg',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV Sample Avg',
|
'performanceOverlay.dvSampleAverage' => 'DV Sample Avg',
|
||||||
|
_ => null,
|
||||||
|
} ?? switch (path) {
|
||||||
'performanceOverlay.maxLuma' => 'Max Luma',
|
'performanceOverlay.maxLuma' => 'Max Luma',
|
||||||
'performanceOverlay.minLuma' => 'Min Luma',
|
'performanceOverlay.minLuma' => 'Min Luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
'performanceOverlay.maxFall' => 'MaxFALL',
|
'performanceOverlay.maxFall' => 'MaxFALL',
|
||||||
'performanceOverlay.cacheUsed' => 'Cache Used',
|
'performanceOverlay.cacheUsed' => 'Cache Used',
|
||||||
_ => null,
|
|
||||||
} ?? switch (path) {
|
|
||||||
'performanceOverlay.cacheLimit' => 'Cache Limit',
|
'performanceOverlay.cacheLimit' => 'Cache Limit',
|
||||||
'performanceOverlay.speed' => 'Speed',
|
'performanceOverlay.speed' => 'Speed',
|
||||||
'performanceOverlay.player' => 'Player',
|
'performanceOverlay.player' => 'Player',
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Mörkt",
|
"darkTheme": "Mörkt",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Biblioteksdensitet",
|
"libraryDensity": "Biblioteksdensitet",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Kompakt",
|
"compact": "Kompakt",
|
||||||
"comfortable": "Luftig",
|
"comfortable": "Luftig",
|
||||||
"tvCornerSpotlightBackdrop": "Bakgrundsbild för utvalt innehåll i hörnet",
|
"tvCornerSpotlightBackdrop": "Bakgrundsbild för utvalt innehåll i hörnet",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size} MB",
|
"bufferSizeMB": "${size} MB",
|
||||||
"bufferSizeAuto": "Automatiskt (rekommenderas)",
|
"bufferSizeAuto": "Automatiskt (rekommenderas)",
|
||||||
"bufferSizeWarning": "${heap} MB minne är tillgängligt. En buffert på ${size} MB kan påverka uppspelningen.",
|
"bufferSizeWarning": "${heap} MB minne är tillgängligt. En buffert på ${size} MB kan påverka uppspelningen.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Standardkvalitet",
|
"defaultQualityTitle": "Standardkvalitet",
|
||||||
"musicQualityTitle": "Musikkvalitet",
|
"musicQualityTitle": "Musikkvalitet",
|
||||||
"subtitleStyling": "Utseende för undertexter",
|
"subtitleStyling": "Utseende för undertexter",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Koyu",
|
"darkTheme": "Koyu",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Kitaplık Yoğunluğu",
|
"libraryDensity": "Kitaplık Yoğunluğu",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Sıkışık",
|
"compact": "Sıkışık",
|
||||||
"comfortable": "Rahat",
|
"comfortable": "Rahat",
|
||||||
"tvCornerSpotlightBackdrop": "Köşe Öne Çıkan Arka Plan",
|
"tvCornerSpotlightBackdrop": "Köşe Öne Çıkan Arka Plan",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size}MB",
|
"bufferSizeMB": "${size}MB",
|
||||||
"bufferSizeAuto": "Otomatik (Önerilen)",
|
"bufferSizeAuto": "Otomatik (Önerilen)",
|
||||||
"bufferSizeWarning": "${heap}MB bellek mevcut. ${size}MB arabellek oynatmayı etkileyebilir.",
|
"bufferSizeWarning": "${heap}MB bellek mevcut. ${size}MB arabellek oynatmayı etkileyebilir.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Varsayılan Kalite",
|
"defaultQualityTitle": "Varsayılan Kalite",
|
||||||
"musicQualityTitle": "Müzik Kalitesi",
|
"musicQualityTitle": "Müzik Kalitesi",
|
||||||
"subtitleStyling": "Altyazı Biçimlendirmesi",
|
"subtitleStyling": "Altyazı Biçimlendirmesi",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "Toʻq",
|
"darkTheme": "Toʻq",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "Kutubxona zichligi",
|
"libraryDensity": "Kutubxona zichligi",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "Ixcham",
|
"compact": "Ixcham",
|
||||||
"comfortable": "Qulay",
|
"comfortable": "Qulay",
|
||||||
"tvCornerSpotlightBackdrop": "Burchak yoritish foni",
|
"tvCornerSpotlightBackdrop": "Burchak yoritish foni",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size} MB",
|
"bufferSizeMB": "${size} MB",
|
||||||
"bufferSizeAuto": "Avtomatik (Tavsiya etilgan)",
|
"bufferSizeAuto": "Avtomatik (Tavsiya etilgan)",
|
||||||
"bufferSizeWarning": "${heap} MB Xotira mavjud. ${size} MB bufer ijroga taʼsir qilishi mumkin.",
|
"bufferSizeWarning": "${heap} MB Xotira mavjud. ${size} MB bufer ijroga taʼsir qilishi mumkin.",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "Standart sifat",
|
"defaultQualityTitle": "Standart sifat",
|
||||||
"musicQualityTitle": "Musiqa sifati",
|
"musicQualityTitle": "Musiqa sifati",
|
||||||
"subtitleStyling": "Subtitr sozlamalari",
|
"subtitleStyling": "Subtitr sozlamalari",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "深色",
|
"darkTheme": "深色",
|
||||||
"oledTheme": "OLED 純黑",
|
"oledTheme": "OLED 純黑",
|
||||||
"libraryDensity": "媒體庫版面配置密度",
|
"libraryDensity": "媒體庫版面配置密度",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "緊湊",
|
"compact": "緊湊",
|
||||||
"comfortable": "舒適",
|
"comfortable": "舒適",
|
||||||
"tvCornerSpotlightBackdrop": "右上角焦點背景圖",
|
"tvCornerSpotlightBackdrop": "右上角焦點背景圖",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size} MB",
|
"bufferSizeMB": "${size} MB",
|
||||||
"bufferSizeAuto": "自動(推薦)",
|
"bufferSizeAuto": "自動(推薦)",
|
||||||
"bufferSizeWarning": "可用記憶體為 ${heap} MB。設定 ${size} MB 緩衝可能影響播放穩定性。",
|
"bufferSizeWarning": "可用記憶體為 ${heap} MB。設定 ${size} MB 緩衝可能影響播放穩定性。",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "預設畫質",
|
"defaultQualityTitle": "預設畫質",
|
||||||
"musicQualityTitle": "音樂品質",
|
"musicQualityTitle": "音樂品質",
|
||||||
"subtitleStyling": "字幕樣式",
|
"subtitleStyling": "字幕樣式",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"darkTheme": "深色",
|
"darkTheme": "深色",
|
||||||
"oledTheme": "OLED",
|
"oledTheme": "OLED",
|
||||||
"libraryDensity": "媒体库密度",
|
"libraryDensity": "媒体库密度",
|
||||||
|
"displayScale": "",
|
||||||
"compact": "紧凑",
|
"compact": "紧凑",
|
||||||
"comfortable": "舒适",
|
"comfortable": "舒适",
|
||||||
"tvCornerSpotlightBackdrop": "右上角聚焦背景图",
|
"tvCornerSpotlightBackdrop": "右上角聚焦背景图",
|
||||||
@@ -169,6 +170,11 @@
|
|||||||
"bufferSizeMB": "${size}MB",
|
"bufferSizeMB": "${size}MB",
|
||||||
"bufferSizeAuto": "自动(推荐)",
|
"bufferSizeAuto": "自动(推荐)",
|
||||||
"bufferSizeWarning": "可用内存 ${heap}MB。${size}MB 缓冲可能影响播放。",
|
"bufferSizeWarning": "可用内存 ${heap}MB。${size}MB 缓冲可能影响播放。",
|
||||||
|
"playbackBuffer": "",
|
||||||
|
"playbackBufferAuto": "",
|
||||||
|
"playbackBufferLarge": "",
|
||||||
|
"playbackBufferExtraLarge": "",
|
||||||
|
"playbackBufferDescription": "",
|
||||||
"defaultQualityTitle": "默认画质",
|
"defaultQualityTitle": "默认画质",
|
||||||
"musicQualityTitle": "音乐音质",
|
"musicQualityTitle": "音乐音质",
|
||||||
"subtitleStyling": "字幕样式",
|
"subtitleStyling": "字幕样式",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class PlayerAndroid extends PlayerBase {
|
|||||||
|
|
||||||
int? _bufferSizeBytes;
|
int? _bufferSizeBytes;
|
||||||
bool _bufferSizeIsAuto = false;
|
bool _bufferSizeIsAuto = false;
|
||||||
|
String _bufferTier = 'auto';
|
||||||
bool _tunnelingEnabled = true;
|
bool _tunnelingEnabled = true;
|
||||||
String _dvConversionMode = 'auto';
|
String _dvConversionMode = 'auto';
|
||||||
bool _audioNormalizationEnabled = false;
|
bool _audioNormalizationEnabled = false;
|
||||||
@@ -115,6 +116,7 @@ class PlayerAndroid extends PlayerBase {
|
|||||||
final result = await invoke<bool>('initialize', {
|
final result = await invoke<bool>('initialize', {
|
||||||
'bufferSizeBytes': _bufferSizeBytes,
|
'bufferSizeBytes': _bufferSizeBytes,
|
||||||
'bufferSizeAuto': _bufferSizeIsAuto,
|
'bufferSizeAuto': _bufferSizeIsAuto,
|
||||||
|
'bufferTier': _bufferTier,
|
||||||
'tunnelingEnabled': _tunnelingEnabled,
|
'tunnelingEnabled': _tunnelingEnabled,
|
||||||
'dvConversionMode': _dvConversionMode,
|
'dvConversionMode': _dvConversionMode,
|
||||||
'audioPassthroughEnabled': _audioPassthroughEnabled,
|
'audioPassthroughEnabled': _audioPassthroughEnabled,
|
||||||
@@ -331,6 +333,12 @@ class PlayerAndroid extends PlayerBase {
|
|||||||
case 'demuxer-max-bytes-auto':
|
case 'demuxer-max-bytes-auto':
|
||||||
_bufferSizeIsAuto = value != 'no';
|
_bufferSizeIsAuto = value != 'no';
|
||||||
break;
|
break;
|
||||||
|
// Not an mpv property. mpv read-ahead is owned by the mpv.conf editor; this tier is
|
||||||
|
// a named ExoPlayer read-ahead depth rather than a duration because the byte cap can
|
||||||
|
// bind first (#1816).
|
||||||
|
case 'exo-buffer-tier':
|
||||||
|
_bufferTier = value;
|
||||||
|
break;
|
||||||
case 'tunneled-playback':
|
case 'tunneled-playback':
|
||||||
_tunnelingEnabled = value != 'no';
|
_tunnelingEnabled = value != 'no';
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
|||||||
if (PlatformDetector.isAppleTV()) _atmosDiagnosticsTile(),
|
if (PlatformDetector.isAppleTV()) _atmosDiagnosticsTile(),
|
||||||
if (exoActive) _dvConversionModeTile(),
|
if (exoActive) _dvConversionModeTile(),
|
||||||
_bufferSizeTile(),
|
_bufferSizeTile(),
|
||||||
|
if (exoActive) _playbackBufferTile(),
|
||||||
_defaultQualityTile(),
|
_defaultQualityTile(),
|
||||||
_musicQualityTile(),
|
_musicQualityTile(),
|
||||||
],
|
],
|
||||||
@@ -433,6 +434,22 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _playbackBufferTile() => SettingSelectionTile<PlaybackBufferTier>(
|
||||||
|
pref: SettingsService.playbackBufferTier,
|
||||||
|
icon: Symbols.hourglass_top_rounded,
|
||||||
|
title: t.settings.playbackBuffer,
|
||||||
|
subtitleBuilder: (tier) => '${_playbackBufferLabel(tier)} · ${t.settings.playbackBufferDescription}',
|
||||||
|
options: PlaybackBufferTier.values
|
||||||
|
.map((tier) => DialogOption(value: tier, title: _playbackBufferLabel(tier)))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
String _playbackBufferLabel(PlaybackBufferTier tier) => switch (tier) {
|
||||||
|
PlaybackBufferTier.auto => t.settings.playbackBufferAuto,
|
||||||
|
PlaybackBufferTier.large => t.settings.playbackBufferLarge,
|
||||||
|
PlaybackBufferTier.extraLarge => t.settings.playbackBufferExtraLarge,
|
||||||
|
};
|
||||||
|
|
||||||
Widget _defaultQualityTile() => SettingSelectionTile<TranscodeQualityPreset>(
|
Widget _defaultQualityTile() => SettingSelectionTile<TranscodeQualityPreset>(
|
||||||
pref: SettingsService.defaultQualityPreset,
|
pref: SettingsService.defaultQualityPreset,
|
||||||
icon: Symbols.high_quality_rounded,
|
icon: Symbols.high_quality_rounded,
|
||||||
|
|||||||
@@ -1193,6 +1193,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
_exitFullscreenOnPlayerClose = settingsService.read(SettingsService.exitFullscreenOnPlayerClose);
|
_exitFullscreenOnPlayerClose = settingsService.read(SettingsService.exitFullscreenOnPlayerClose);
|
||||||
_rewindOnResume = settingsService.read(SettingsService.rewindOnResume);
|
_rewindOnResume = settingsService.read(SettingsService.rewindOnResume);
|
||||||
final bufferSizeMB = settingsService.read(SettingsService.bufferSize);
|
final bufferSizeMB = settingsService.read(SettingsService.bufferSize);
|
||||||
|
final playbackBufferTier = settingsService.read(SettingsService.playbackBufferTier);
|
||||||
final enableHardwareDecoding = settingsService.read(SettingsService.enableHardwareDecoding);
|
final enableHardwareDecoding = settingsService.read(SettingsService.enableHardwareDecoding);
|
||||||
final debugLoggingEnabled = settingsService.read(SettingsService.enableDebugLogging);
|
final debugLoggingEnabled = settingsService.read(SettingsService.enableDebugLogging);
|
||||||
final useExoPlayer = settingsService.read(SettingsService.useExoPlayer);
|
final useExoPlayer = settingsService.read(SettingsService.useExoPlayer);
|
||||||
@@ -1285,6 +1286,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
if (Platform.isAndroid && useExoPlayer) {
|
if (Platform.isAndroid && useExoPlayer) {
|
||||||
final tunneledPlayback = settingsService.read(SettingsService.tunneledPlayback);
|
final tunneledPlayback = settingsService.read(SettingsService.tunneledPlayback);
|
||||||
await currentPlayer.setProperty('tunneled-playback', tunneledPlayback ? 'yes' : 'no');
|
await currentPlayer.setProperty('tunneled-playback', tunneledPlayback ? 'yes' : 'no');
|
||||||
|
await currentPlayer.setProperty('exo-buffer-tier', playbackBufferTier.nativeValue);
|
||||||
}
|
}
|
||||||
if ((Platform.isAndroid && useExoPlayer) || Platform.isIOS || Platform.isMacOS) {
|
if ((Platform.isAndroid && useExoPlayer) || Platform.isIOS || Platform.isMacOS) {
|
||||||
final dvConversionMode = settingsService.read(SettingsService.dvConversionMode);
|
final dvConversionMode = settingsService.read(SettingsService.dvConversionMode);
|
||||||
|
|||||||
@@ -83,6 +83,16 @@ extension DvConversionModePreferenceNativeValue on DvConversionModePreference {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PlaybackBufferTier { auto, large, extraLarge }
|
||||||
|
|
||||||
|
extension PlaybackBufferTierNativeValue on PlaybackBufferTier {
|
||||||
|
String get nativeValue => switch (this) {
|
||||||
|
PlaybackBufferTier.auto => 'auto',
|
||||||
|
PlaybackBufferTier.large => 'large',
|
||||||
|
PlaybackBufferTier.extraLarge => 'extra_large',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const String _bufferSizeMigratedKey = 'buffer_size_migrated_to_auto';
|
const String _bufferSizeMigratedKey = 'buffer_size_migrated_to_auto';
|
||||||
const String _legacyUseSeasonPosterKey = 'use_season_poster';
|
const String _legacyUseSeasonPosterKey = 'use_season_poster';
|
||||||
const String _legacyMpvConfigEntriesKey = 'mpv_config_entries';
|
const String _legacyMpvConfigEntriesKey = 'mpv_config_entries';
|
||||||
@@ -504,6 +514,11 @@ class SettingsService extends BaseSharedPreferencesService {
|
|||||||
static const exitFullscreenOnPlayerClose = BoolPref('exit_fullscreen_on_player_close');
|
static const exitFullscreenOnPlayerClose = BoolPref('exit_fullscreen_on_player_close');
|
||||||
|
|
||||||
static const bufferSize = _BufferSizePref();
|
static const bufferSize = _BufferSizePref();
|
||||||
|
static const playbackBufferTier = EnumPref<PlaybackBufferTier>(
|
||||||
|
'playback_buffer_tier',
|
||||||
|
values: PlaybackBufferTier.values,
|
||||||
|
defaultValue: PlaybackBufferTier.auto,
|
||||||
|
);
|
||||||
static const libraryDensity = _LibraryDensityPref();
|
static const libraryDensity = _LibraryDensityPref();
|
||||||
static const automotiveUiScale = _AutomotiveUiScalePref();
|
static const automotiveUiScale = _AutomotiveUiScalePref();
|
||||||
static const tvCornerSpotlightBackdrop = BoolPref('tv_corner_spotlight_backdrop');
|
static const tvCornerSpotlightBackdrop = BoolPref('tv_corner_spotlight_backdrop');
|
||||||
@@ -932,6 +947,7 @@ class SettingsService extends BaseSharedPreferencesService {
|
|||||||
themeMode,
|
themeMode,
|
||||||
videoPlayerNavigationEnabled,
|
videoPlayerNavigationEnabled,
|
||||||
bufferSize,
|
bufferSize,
|
||||||
|
playbackBufferTier,
|
||||||
libraryDensity,
|
libraryDensity,
|
||||||
automotiveUiScale,
|
automotiveUiScale,
|
||||||
tvCornerSpotlightBackdrop,
|
tvCornerSpotlightBackdrop,
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ class _PlayerPerformanceOverlayState extends State<PlayerPerformanceOverlay> {
|
|||||||
_metric(t.fileInfo.duration, _stats.cacheDurationFormatted),
|
_metric(t.fileInfo.duration, _stats.cacheDurationFormatted),
|
||||||
if (isMpv) _metric(t.performanceOverlay.cacheUsed, _stats.cacheUsedFormatted),
|
if (isMpv) _metric(t.performanceOverlay.cacheUsed, _stats.cacheUsedFormatted),
|
||||||
if (isMpv) _metric(t.performanceOverlay.cacheLimit, _stats.cacheLimitFormatted),
|
if (isMpv) _metric(t.performanceOverlay.cacheLimit, _stats.cacheLimitFormatted),
|
||||||
|
if (!isMpv && _stats.hasValidBufferLimits)
|
||||||
|
_metric(t.performanceOverlay.cacheLimit, _stats.bufferLimitsFormatted),
|
||||||
if (isMpv) _metric(t.performanceOverlay.speed, _stats.cacheSpeedFormatted),
|
if (isMpv) _metric(t.performanceOverlay.speed, _stats.cacheSpeedFormatted),
|
||||||
]),
|
]),
|
||||||
_buildSection(Symbols.apps_rounded, t.performanceOverlay.app, [
|
_buildSection(Symbols.apps_rounded, t.performanceOverlay.app, [
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ class PerformanceStats {
|
|||||||
final int? cacheLimit;
|
final int? cacheLimit;
|
||||||
final double? cacheSpeed;
|
final double? cacheSpeed;
|
||||||
final double? cacheDuration;
|
final double? cacheDuration;
|
||||||
|
final int? bufferTargetBytes;
|
||||||
|
final int? bufferMaxMs;
|
||||||
|
|
||||||
// DV conversion
|
// DV conversion
|
||||||
final bool dvConversionActive;
|
final bool dvConversionActive;
|
||||||
@@ -106,6 +108,8 @@ class PerformanceStats {
|
|||||||
this.cacheLimit,
|
this.cacheLimit,
|
||||||
this.cacheSpeed,
|
this.cacheSpeed,
|
||||||
this.cacheDuration,
|
this.cacheDuration,
|
||||||
|
this.bufferTargetBytes,
|
||||||
|
this.bufferMaxMs,
|
||||||
this.dvConversionActive = false,
|
this.dvConversionActive = false,
|
||||||
this.dvConversionMode = '',
|
this.dvConversionMode = '',
|
||||||
this.dvConvertedRpus,
|
this.dvConvertedRpus,
|
||||||
@@ -157,6 +161,8 @@ class PerformanceStats {
|
|||||||
cacheLimit = null,
|
cacheLimit = null,
|
||||||
cacheSpeed = null,
|
cacheSpeed = null,
|
||||||
cacheDuration = null,
|
cacheDuration = null,
|
||||||
|
bufferTargetBytes = null,
|
||||||
|
bufferMaxMs = null,
|
||||||
dvConversionActive = false,
|
dvConversionActive = false,
|
||||||
dvConversionMode = '',
|
dvConversionMode = '',
|
||||||
dvConvertedRpus = null,
|
dvConvertedRpus = null,
|
||||||
@@ -183,6 +189,17 @@ class PerformanceStats {
|
|||||||
return '${mbps.toStringAsFixed(1)} Mbps';
|
return '${mbps.toStringAsFixed(1)} Mbps';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Both ExoPlayer read-ahead ceilings: the duration target and the hard byte cap.
|
||||||
|
/// The smaller one binds, so showing both explains a duration setting that
|
||||||
|
/// appears to have no effect on high-bitrate media.
|
||||||
|
String get bufferLimitsFormatted {
|
||||||
|
if (bufferMaxMs == null || bufferMaxMs! <= 0) return 'N/A';
|
||||||
|
final duration = '${bufferMaxMs! ~/ 1000}s';
|
||||||
|
if (bufferTargetBytes == null) return duration;
|
||||||
|
final targetBufferMb = bufferTargetBytes! ~/ (1024 * 1024);
|
||||||
|
return '$duration / ${targetBufferMb}MB';
|
||||||
|
}
|
||||||
|
|
||||||
/// Format audio bitrate in kbps.
|
/// Format audio bitrate in kbps.
|
||||||
String get audioBitrateFormatted {
|
String get audioBitrateFormatted {
|
||||||
if (audioBitrate == null || audioBitrate == 0) return 'N/A';
|
if (audioBitrate == null || audioBitrate == 0) return 'N/A';
|
||||||
@@ -377,6 +394,8 @@ class PerformanceStats {
|
|||||||
return videoBitrate != null && videoBitrate! > 0;
|
return videoBitrate != null && videoBitrate! > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get hasValidBufferLimits => bufferMaxMs != null && bufferMaxMs! > 0;
|
||||||
|
|
||||||
/// Check if audio bitrate is valid (not null, not negative, not zero).
|
/// Check if audio bitrate is valid (not null, not negative, not zero).
|
||||||
bool get hasValidAudioBitrate {
|
bool get hasValidAudioBitrate {
|
||||||
return audioBitrate != null && audioBitrate! > 0;
|
return audioBitrate != null && audioBitrate! > 0;
|
||||||
|
|||||||
@@ -211,6 +211,8 @@ class PerformanceStatsService {
|
|||||||
frameDropCount: statsMap['videoDroppedFrames'] as int?,
|
frameDropCount: statsMap['videoDroppedFrames'] as int?,
|
||||||
// Buffer metrics - convert ms to seconds for duration
|
// Buffer metrics - convert ms to seconds for duration
|
||||||
cacheDuration: ((statsMap['totalBufferedDurationMs'] as int?) ?? 0) / 1000.0,
|
cacheDuration: ((statsMap['totalBufferedDurationMs'] as int?) ?? 0) / 1000.0,
|
||||||
|
bufferTargetBytes: statsMap['bufferTargetBytes'] as int?,
|
||||||
|
bufferMaxMs: statsMap['bufferMaxMs'] as int?,
|
||||||
// DV conversion
|
// DV conversion
|
||||||
dvConversionActive: statsMap['dvConversionActive'] == true,
|
dvConversionActive: statsMap['dvConversionActive'] == true,
|
||||||
dvConversionMode: statsMap['dvConversionMode'] as String? ?? '',
|
dvConversionMode: statsMap['dvConversionMode'] as String? ?? '',
|
||||||
|
|||||||
@@ -82,5 +82,20 @@ void main() {
|
|||||||
final args = initialize.arguments as Map<Object?, Object?>;
|
final args = initialize.arguments as Map<Object?, Object?>;
|
||||||
expect(args['bufferSizeAuto'], isFalse);
|
expect(args['bufferSizeAuto'], isFalse);
|
||||||
expect(args['bufferSizeBytes'], isNull);
|
expect(args['bufferSizeBytes'], isNull);
|
||||||
|
// Playback Buffer is also init-only: when absent, native must receive an explicit Auto wire value.
|
||||||
|
expect(args['bufferTier'], 'auto');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('an explicit Playback Buffer tier reaches native initialization', () async {
|
||||||
|
final initialize = await _captureInitialize(
|
||||||
|
configure: (player) async {
|
||||||
|
await player.setProperty('exo-buffer-tier', PlaybackBufferTier.extraLarge.nativeValue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final args = initialize.arguments as Map<Object?, Object?>;
|
||||||
|
// The synthetic property is init-only; losing it would silently restore the native Auto tier.
|
||||||
|
expect(args['bufferTier'], 'extra_large');
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user