Files
plezy/test/mpv/player_android_buffer_size_test.dart
T
edde746 660e375248 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
2026-08-07 08:43:47 +02:00

102 lines
4.1 KiB
Dart

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);
// 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');
});
}