fix(player): stop handing ExoPlayer the demuxer's buffer budget on Auto
On Auto, Dart derives a buffer size for mpv's demuxer from the device heap and sets it as `demuxer-max-bytes`. The Android player forwarded that same number to `DefaultLoadControl.setTargetBufferBytes`, so ExoPlayer's sample allocator was sized by a tier table written for a different consumer: 64MB on any device whose large heap is 512MB or less, which every Shield is. `targetBufferBytes` is a byte cap, so the media it represents collapses as bitrate rises — 64MB is 53s of a 10 Mbit/s stream but 5.2s of a 103 Mbit/s UHD remux. With `prioritizeTimeOverSizeThresholds` false the cap is hard: `shouldContinueLoading` returns false the moment the allocator reaches it no matter how little media that is, and `shouldStartPlayback` reports READY off the same byte term. Read-ahead that short starves the audio sink in bursts, and on a passthrough route that is enough to keep the AudioTrack from ever starting — the track initializes, accepts one access unit and never renders a frame. Because an enabled audio renderer owns the MediaClock, the whole player freezes and the black-screen watchdog then blames the video decoder and drops the session to mpv. Size the LoadControl target natively instead, from what actually bounds `DefaultAllocator`: the Java heap. `min(media3's own default for a video+audio selection, largeMemoryClass/4, availMem/4)` with a 32MB floor, the lowest tier that has already shipped. The quarter matches the threshold the Buffer Size setting already warns at, and the media3 default is a ceiling — this is not "buffer more than upstream", it is "stop buffering less". Deliberately not bitrate-aware, because the LoadControl is built during initialize, before any media is opened. `bufferSizeAuto` carries the distinction over the channel; `bufferSizeBytes` still travels with it because the plugin's mpv fallback replays it as a real demuxer property, and an explicit Buffer Size choice is still honoured verbatim. Confirmed against the hardware in the 2.9.1 passthrough report. That reporter's own log is a natural A/B: three runs at 64MB fail with `0 frames rendered after 8002ms`, spanning both DV conversion modes and both tunneling states, while the single run after he manually selected 128MB logs `Position advancing` and renders. Reproduced on the same Shield model with codec and bitrate held fixed and only the cap varied — 6s of audio demand stalls at 64MiB and plays at 128MiB, 4 of 4 predictions, with read-ahead measured off an injected DefaultAllocator at 65 664 and 131 776 KiB. That device reports `dalvik.vm.heapsize` 512m, so the heap term binds first at every free-memory level in his log and Auto now derives exactly the 128MB he had to pick by hand; the shipped path logs `Buffer: 128MB limit (auto, heap=512MB, available=568MB)` where it previously logged 64MB.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
private const val MIB = 1024 * 1024
|
||||
|
||||
class LoadControlPolicyTest {
|
||||
|
||||
// autoTargetBufferBytes
|
||||
|
||||
@Test
|
||||
fun neverExceedsMedia3sOwnTargetEvenWithHugeMemory() {
|
||||
assertEquals(
|
||||
LoadControlPolicy.MEDIA3_DEFAULT_TARGET_BYTES,
|
||||
LoadControlPolicy.autoTargetBufferBytes(largeHeapMB = 4096, availableMB = 8192)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun heapBoundsTheTargetOnAShieldClassDevice() {
|
||||
// The actual #1618 defect: the shipped tiers handed this device a flat 64MB, under half
|
||||
// of media3's own choice. largeMemoryClass 512MB, ~1GB free, so the heap binds at
|
||||
// 512/4 = 128MB — exactly what the reporter had to select by hand.
|
||||
assertEquals(
|
||||
128 * MIB,
|
||||
LoadControlPolicy.autoTargetBufferBytes(largeHeapMB = 512, availableMB = 990)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun freeMemoryBoundsTheTargetWhenItIsTighterThanTheHeap() {
|
||||
assertEquals(
|
||||
64 * MIB,
|
||||
LoadControlPolicy.autoTargetBufferBytes(largeHeapMB = 512, availableMB = 256)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun floorWinsOverBothBudgetsSoReadAheadCannotCollapse() {
|
||||
// Going under the floor is what starves the sink on high-bitrate content; the
|
||||
// allocator only grows into the target when the content is dense enough to need it.
|
||||
assertEquals(
|
||||
LoadControlPolicy.MIN_TARGET_BYTES,
|
||||
LoadControlPolicy.autoTargetBufferBytes(largeHeapMB = 64, availableMB = 48)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknownMemoryFallsBackToMedia3sTarget() {
|
||||
assertEquals(
|
||||
LoadControlPolicy.MEDIA3_DEFAULT_TARGET_BYTES,
|
||||
LoadControlPolicy.autoTargetBufferBytes(largeHeapMB = 0, availableMB = 0)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknownHeapStillRespectsFreeMemory() {
|
||||
assertEquals(
|
||||
64 * MIB,
|
||||
LoadControlPolicy.autoTargetBufferBytes(largeHeapMB = -1, availableMB = 256)
|
||||
)
|
||||
}
|
||||
|
||||
// readAheadSeconds
|
||||
|
||||
@Test
|
||||
fun readAheadReportsSecondsAtAKnownBitrate() {
|
||||
// 64MiB of the #1618 stream (103_341 kbps) is ~5.2s — under the 15s minBufferMs, so the
|
||||
// byte cap, not the time threshold, is what stops the loader.
|
||||
val seconds = LoadControlPolicy.readAheadSeconds(64 * MIB, 103_341_000L)!!
|
||||
assertEquals(5.19, seconds, 0.01)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readAheadDoublesWithTheTarget() {
|
||||
val seconds = LoadControlPolicy.readAheadSeconds(128 * MIB, 103_341_000L)!!
|
||||
assertEquals(10.39, seconds, 0.01)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readAheadIsUnknownWithoutABitrate() {
|
||||
assertNull(LoadControlPolicy.readAheadSeconds(64 * MIB, 0L))
|
||||
assertNull(LoadControlPolicy.readAheadSeconds(64 * MIB, -1L))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user