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,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