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:
@@ -523,6 +523,7 @@ class ExoPlayerCore(private val activity: Activity) :
|
|||||||
|
|
||||||
fun initialize(
|
fun initialize(
|
||||||
bufferSizeBytes: Int? = null,
|
bufferSizeBytes: Int? = null,
|
||||||
|
bufferSizeAuto: Boolean = false,
|
||||||
tunnelingEnabled: Boolean = true,
|
tunnelingEnabled: Boolean = true,
|
||||||
audioPassthroughEnabled: Boolean = false
|
audioPassthroughEnabled: Boolean = false
|
||||||
): Boolean {
|
): Boolean {
|
||||||
@@ -731,23 +732,20 @@ class ExoPlayerCore(private val activity: Activity) :
|
|||||||
.toTypedArray()
|
.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute memory-aware buffer limits to prevent CCodec OOM crashes
|
// Buffer budget. `bufferSizeBytes` carries the user's explicit Buffer Size choice; on
|
||||||
|
// Auto it still arrives (Dart derives it for mpv's demuxer, which shares the property)
|
||||||
|
// but `bufferSizeAuto` says to ignore it here, because mpv's demuxer and ExoPlayer's
|
||||||
|
// sample allocator have different shapes and different failure modes.
|
||||||
val activityManager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
val activityManager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||||
val memoryInfo = ActivityManager.MemoryInfo()
|
val memoryInfo = ActivityManager.MemoryInfo()
|
||||||
activityManager.getMemoryInfo(memoryInfo)
|
activityManager.getMemoryInfo(memoryInfo)
|
||||||
val availableMB = memoryInfo.availMem / (1024 * 1024)
|
val availableMB = (memoryInfo.availMem / (1024 * 1024)).toInt()
|
||||||
|
val largeHeapMB = activityManager.largeMemoryClass
|
||||||
|
|
||||||
val targetBufferBytes = if (bufferSizeBytes != null && bufferSizeBytes > 0) {
|
val targetBufferBytes = if (!bufferSizeAuto && bufferSizeBytes != null && bufferSizeBytes > 0) {
|
||||||
bufferSizeBytes
|
bufferSizeBytes
|
||||||
} else {
|
} else {
|
||||||
// Scale buffer to available memory to reduce hardware decoder pressure.
|
LoadControlPolicy.autoTargetBufferBytes(largeHeapMB, availableMB)
|
||||||
// Larger buffers reduce oscillation frequency at high bitrates (50-100Mbps).
|
|
||||||
when {
|
|
||||||
availableMB <= 512 -> 30 * 1024 * 1024
|
|
||||||
availableMB <= 1024 -> 80 * 1024 * 1024
|
|
||||||
availableMB <= 2048 -> 120 * 1024 * 1024
|
|
||||||
else -> 200 * 1024 * 1024
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val loadControl = DefaultLoadControl.Builder().apply {
|
val loadControl = DefaultLoadControl.Builder().apply {
|
||||||
@@ -759,7 +757,12 @@ class ExoPlayerCore(private val activity: Activity) :
|
|||||||
setBufferDurationsMs(30_000, 60_000, 1_000, 5_000)
|
setBufferDurationsMs(30_000, 60_000, 1_000, 5_000)
|
||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
emitLog("info", "init", "Buffer: ${targetBufferBytes / 1024 / 1024}MB limit, available=${availableMB}MB, tunneling=$tunnelingUserEnabled, dataSource=$dataSourceLabel")
|
emitLog(
|
||||||
|
"info",
|
||||||
|
"init",
|
||||||
|
"Buffer: ${targetBufferBytes / 1024 / 1024}MB limit (${if (bufferSizeAuto) "auto" else "manual"}, " +
|
||||||
|
"heap=${largeHeapMB}MB, available=${availableMB}MB), tunneling=$tunnelingUserEnabled, dataSource=$dataSourceLabel"
|
||||||
|
)
|
||||||
|
|
||||||
exoPlayer = ExoPlayer.Builder(activity)
|
exoPlayer = ExoPlayer.Builder(activity)
|
||||||
.setTrackSelector(trackSelector!!)
|
.setTrackSelector(trackSelector!!)
|
||||||
|
|||||||
@@ -285,6 +285,10 @@ class ExoPlayerPlugin :
|
|||||||
}
|
}
|
||||||
|
|
||||||
val bufferSizeBytes = call.argument<Int>("bufferSizeBytes")
|
val bufferSizeBytes = call.argument<Int>("bufferSizeBytes")
|
||||||
|
// Auto sizing is decided natively (LoadControlPolicy). `bufferSizeBytes` still arrives
|
||||||
|
// on Auto because Dart derives one for mpv's demuxer, which shares the property, and
|
||||||
|
// the fallback replay below needs it.
|
||||||
|
val bufferSizeAuto = call.argument<Boolean>("bufferSizeAuto") ?: false
|
||||||
val tunnelingEnabled = call.argument<Boolean>("tunnelingEnabled") ?: true
|
val tunnelingEnabled = call.argument<Boolean>("tunnelingEnabled") ?: true
|
||||||
val dvConversionMode = call.argument<String>("dvConversionMode") ?: "auto"
|
val dvConversionMode = call.argument<String>("dvConversionMode") ?: "auto"
|
||||||
val audioPassthroughEnabled = call.argument<Boolean>("audioPassthroughEnabled") ?: false
|
val audioPassthroughEnabled = call.argument<Boolean>("audioPassthroughEnabled") ?: false
|
||||||
@@ -323,6 +327,7 @@ class ExoPlayerPlugin :
|
|||||||
playerCore = core
|
playerCore = core
|
||||||
val success = core.initialize(
|
val success = core.initialize(
|
||||||
bufferSizeBytes = bufferSizeBytes,
|
bufferSizeBytes = bufferSizeBytes,
|
||||||
|
bufferSizeAuto = bufferSizeAuto,
|
||||||
tunnelingEnabled = tunnelingEnabled,
|
tunnelingEnabled = tunnelingEnabled,
|
||||||
audioPassthroughEnabled = audioPassthroughEnabled
|
audioPassthroughEnabled = audioPassthroughEnabled
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.edde746.plezy.exoplayer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto sizing for [androidx.media3.exoplayer.DefaultLoadControl]'s `targetBufferBytes` (#1618).
|
||||||
|
*
|
||||||
|
* A byte cap collapses as bitrate rises: 64 MiB 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, so
|
||||||
|
* read-ahead that short starves the audio sink in bursts — enough, on some routes, to keep a
|
||||||
|
* passthrough AudioTrack from ever starting.
|
||||||
|
*
|
||||||
|
* The tiers this replaced came from mpv demuxer OOM tuning and handed ExoPlayer a flat
|
||||||
|
* 64 MiB, under half of what media3 would pick for the same selection
|
||||||
|
* ([MEDIA3_DEFAULT_TARGET_BYTES]). The fix is not "buffer more than upstream", it is "stop
|
||||||
|
* buffering less unless the heap requires it".
|
||||||
|
*
|
||||||
|
* Not bitrate-aware: the `LoadControl` is built during `initialize`, before any media is
|
||||||
|
* opened, so a byte budget is all that is knowable.
|
||||||
|
*/
|
||||||
|
internal object LoadControlPolicy {
|
||||||
|
private const val MIB = 1024 * 1024
|
||||||
|
|
||||||
|
/**
|
||||||
|
* media3's own `calculateTargetBufferBytes` for a video + audio selection: 2000 + 200
|
||||||
|
* segments at `C.DEFAULT_BUFFER_SEGMENT_SIZE` (64 KiB). A ceiling, never exceeded here.
|
||||||
|
*/
|
||||||
|
const val MEDIA3_DEFAULT_TARGET_BYTES = 2200 * 64 * 1024
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Floor, kept at the lowest tier that has already shipped, and it wins over the budgets
|
||||||
|
* below — going under it reintroduces the starvation this policy exists to prevent.
|
||||||
|
*/
|
||||||
|
const val MIN_TARGET_BYTES = 32 * MIB
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fraction of a memory budget the allocator may claim. Matches the threshold the Buffer
|
||||||
|
* Size setting already warns at (`value > heapMB / 4`).
|
||||||
|
*/
|
||||||
|
private const val BUDGET_DIVISOR = 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param largeHeapMB `ActivityManager.largeMemoryClass` — the hard Java-heap ceiling for
|
||||||
|
* this process, which is what bounds `DefaultAllocator` (it hands out `byte[]`).
|
||||||
|
* Non-positive when unknown.
|
||||||
|
* @param availableMB `ActivityManager.MemoryInfo.availMem`, so a device that is currently
|
||||||
|
* under pressure does not get sized purely off its theoretical heap. Non-positive when
|
||||||
|
* unknown.
|
||||||
|
*/
|
||||||
|
fun autoTargetBufferBytes(largeHeapMB: Int, availableMB: Int): Int {
|
||||||
|
var budget = MEDIA3_DEFAULT_TARGET_BYTES.toLong()
|
||||||
|
if (largeHeapMB > 0) budget = minOf(budget, largeHeapMB.toLong() / BUDGET_DIVISOR * MIB)
|
||||||
|
if (availableMB > 0) budget = minOf(budget, availableMB.toLong() / BUDGET_DIVISOR * MIB)
|
||||||
|
return maxOf(budget, MIN_TARGET_BYTES.toLong()).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Seconds of media a budget covers at [bitrateBps], for logs. Null when unknown. */
|
||||||
|
fun readAheadSeconds(targetBufferBytes: Int, bitrateBps: Long): Double? {
|
||||||
|
if (bitrateBps <= 0L) return null
|
||||||
|
return targetBufferBytes * 8.0 / bitrateBps
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ class PlayerAndroid extends PlayerBase {
|
|||||||
static const _eventChannel = EventChannel('com.plezy/exo_player/events');
|
static const _eventChannel = EventChannel('com.plezy/exo_player/events');
|
||||||
|
|
||||||
int? _bufferSizeBytes;
|
int? _bufferSizeBytes;
|
||||||
|
bool _bufferSizeIsAuto = false;
|
||||||
bool _tunnelingEnabled = true;
|
bool _tunnelingEnabled = true;
|
||||||
String _dvConversionMode = 'auto';
|
String _dvConversionMode = 'auto';
|
||||||
bool _audioNormalizationEnabled = false;
|
bool _audioNormalizationEnabled = false;
|
||||||
@@ -100,6 +101,7 @@ class PlayerAndroid extends PlayerBase {
|
|||||||
try {
|
try {
|
||||||
final result = await invoke<bool>('initialize', {
|
final result = await invoke<bool>('initialize', {
|
||||||
'bufferSizeBytes': _bufferSizeBytes,
|
'bufferSizeBytes': _bufferSizeBytes,
|
||||||
|
'bufferSizeAuto': _bufferSizeIsAuto,
|
||||||
'tunnelingEnabled': _tunnelingEnabled,
|
'tunnelingEnabled': _tunnelingEnabled,
|
||||||
'dvConversionMode': _dvConversionMode,
|
'dvConversionMode': _dvConversionMode,
|
||||||
'audioPassthroughEnabled': _audioPassthroughEnabled,
|
'audioPassthroughEnabled': _audioPassthroughEnabled,
|
||||||
@@ -287,6 +289,12 @@ class PlayerAndroid extends PlayerBase {
|
|||||||
case 'demuxer-max-bytes':
|
case 'demuxer-max-bytes':
|
||||||
_bufferSizeBytes = int.tryParse(value);
|
_bufferSizeBytes = int.tryParse(value);
|
||||||
break;
|
break;
|
||||||
|
// Not an mpv property. The heap tiers Dart derives for mpv's demuxer are the wrong
|
||||||
|
// shape for ExoPlayer's sample allocator, so on Auto the native side sizes its own
|
||||||
|
// LoadControl target instead of reusing `demuxer-max-bytes` (#1618).
|
||||||
|
case 'demuxer-max-bytes-auto':
|
||||||
|
_bufferSizeIsAuto = value != 'no';
|
||||||
|
break;
|
||||||
case 'tunneled-playback':
|
case 'tunneled-playback':
|
||||||
_tunnelingEnabled = value != 'no';
|
_tunnelingEnabled = value != 'no';
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1135,6 +1135,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
}
|
}
|
||||||
await currentPlayer.setProperty('demuxer-max-bytes', '${autoForwardMB * 1024 * 1024}');
|
await currentPlayer.setProperty('demuxer-max-bytes', '${autoForwardMB * 1024 * 1024}');
|
||||||
await currentPlayer.setProperty('demuxer-max-back-bytes', '${autoBackMB * 1024 * 1024}');
|
await currentPlayer.setProperty('demuxer-max-back-bytes', '${autoBackMB * 1024 * 1024}');
|
||||||
|
// These tiers size mpv's demuxer. ExoPlayer's LoadControl allocator is a
|
||||||
|
// different consumer — a flat byte cap there collapses to a few seconds of
|
||||||
|
// read-ahead on a 100 Mbps remux — so let the native side derive its own
|
||||||
|
// target on Auto (#1618).
|
||||||
|
await currentPlayer.setProperty('demuxer-max-bytes-auto', 'yes');
|
||||||
} else {
|
} else {
|
||||||
// Manual mode: cap back-buffer relative to heap if 1/4 ratio is too high
|
// Manual mode: cap back-buffer relative to heap if 1/4 ratio is too high
|
||||||
final maxBackBytes = min(bufferSizeMB * 1024 * 1024 ~/ 4, autoBackMB * 1024 * 1024);
|
final maxBackBytes = min(bufferSizeMB * 1024 * 1024 ~/ 4, autoBackMB * 1024 * 1024);
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.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 Buffer Size contract between Dart and `ExoPlayerPlugin` (#1618).
|
||||||
|
///
|
||||||
|
/// Auto still sends `demuxer-max-bytes`, because mpv's demuxer shares the property and the
|
||||||
|
/// plugin's mpv fallback replays it. Only `bufferSizeAuto` tells the native side that the
|
||||||
|
/// value was derived for the demuxer and that `LoadControlPolicy` should size ExoPlayer's
|
||||||
|
/// allocator instead. Getting that flag wrong is silent: playback still works, just with the
|
||||||
|
/// 64MB cap that starves a high-bitrate passthrough track.
|
||||||
|
Future<MethodCall> _captureInitialize({required Future<void> Function(PlayerAndroid player) configure}) async {
|
||||||
|
late MethodCall initialize;
|
||||||
|
await withMockPlayerChannels(
|
||||||
|
methodChannelName: 'com.plezy/exo_player',
|
||||||
|
eventChannelName: 'com.plezy/exo_player/events',
|
||||||
|
methodHandler: (call) async {
|
||||||
|
if (call.method == 'initialize') initialize = call;
|
||||||
|
return call.method == 'initialize' ? true : null;
|
||||||
|
},
|
||||||
|
testBody: () async {
|
||||||
|
final player = PlayerAndroid();
|
||||||
|
try {
|
||||||
|
await configure(player);
|
||||||
|
// requestAudioFocus is what actually triggers native initialize; the screen relies
|
||||||
|
// on that ordering so every setProperty above is cached first.
|
||||||
|
await player.requestAudioFocus();
|
||||||
|
} finally {
|
||||||
|
await player.dispose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return initialize;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
resetSharedPreferencesForTest();
|
||||||
|
SettingsService.resetForTesting();
|
||||||
|
await SettingsService.getInstance();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Auto tells the native side to size its own LoadControl target', () async {
|
||||||
|
final initialize = await _captureInitialize(
|
||||||
|
configure: (player) async {
|
||||||
|
// What video_player_screen sends on Auto for a 512MB-heap device.
|
||||||
|
await player.setProperty('demuxer-max-bytes', '${64 * 1024 * 1024}');
|
||||||
|
await player.setProperty('demuxer-max-bytes-auto', 'yes');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final args = initialize.arguments as Map<Object?, Object?>;
|
||||||
|
expect(args['bufferSizeAuto'], isTrue);
|
||||||
|
// Still forwarded: the plugin's mpv fallback replays it as a real demuxer property.
|
||||||
|
expect(args['bufferSizeBytes'], 64 * 1024 * 1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an explicit Buffer Size choice is never overridden by Auto sizing', () async {
|
||||||
|
final initialize = await _captureInitialize(
|
||||||
|
configure: (player) async {
|
||||||
|
await player.setProperty('demuxer-max-bytes', '${128 * 1024 * 1024}');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final args = initialize.arguments as Map<Object?, Object?>;
|
||||||
|
expect(args['bufferSizeAuto'], isFalse);
|
||||||
|
expect(args['bufferSizeBytes'], 128 * 1024 * 1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an unknown heap leaves no byte cap, so the native side still picks Auto', () async {
|
||||||
|
// video_player_screen skips the whole tier block when getHeapSize() fails, so neither
|
||||||
|
// property is ever set. bufferSizeBytes must stay null rather than defaulting to a
|
||||||
|
// number the native side would treat as a deliberate choice.
|
||||||
|
final initialize = await _captureInitialize(configure: (_) async {});
|
||||||
|
|
||||||
|
final args = initialize.arguments as Map<Object?, Object?>;
|
||||||
|
expect(args['bufferSizeAuto'], isFalse);
|
||||||
|
expect(args['bufferSizeBytes'], isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user