Files
plezy/test/mpv/player_android_buffer_tier_test.dart
T
edde746 a1c98b9d27 feat(player): render Android mpv video on the MediaCodec plane
Android mpv drew every frame through vo=gpu: an ImageReader/GLES copy
pinned to 8-bit RGB0 with a 100 ms timed wait per frame, which
truncated 10-bit and HDR output, raced acquireLatestImage, and
performed badly on low-end TVs.

Video now scans out on a SurfaceFlinger video plane through the fork's
vo=mediacodec: decoder buffers are queued to the surface at their
target PTS (av_mediacodec_release_buffer_at_time), so 10-bit and HDR
dataspaces reach the display untouched and frame pacing no longer
depends on GL vsync. Subtitles and OSD render on a sibling transparent
surface presented for the same PTS, frame-locked by construction.

The plane takes decoder buffers only, so software-decoded video stays
on a GL vo, with a chain-failure watchdog re-initializing the output
when the plane refuses a stream mid-session. The GL fallback is vo=gpu
- gpu-next breaks the Tegra GLES linker (#2010) - and GpuVoPolicy
selects gpu-next only when Dolby Vision RPU reshaping needs
libplacebo. AV1 film grain is applied in the decoder
(vd-lavc-film-grain=cpu) because the GL raster grain fallback is not
available on this path.

Pins libmpv-android v1.1.3, which carries the fork patches this path
rides on: the vo itself, BT2020-PQ EGL window surfaces, GLES direct
rendering treated as slow (H.264 Hi10P software decode on Tegra went
from 0.3x realtime with wrong colors to 1.0x with correct 10-bit
output), and multichannel IEC61937 for ao_audiotrack.

Verified on Shield Pro (Tegra/GLES), Pixel 7 (Mali), Box R 4K Plus
(Amlogic armv7), and Galaxy Z Fold3 (Adreno): 520 play/teardown cycles
and 3 hours of continuous 4K HDR playback; HDR engagement confirmed
via SurfaceFlinger dataspace BT2020_ITU_PQ.
2026-08-26 18:59:19 +02:00

68 lines
2.7 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 Playback Buffer contract between Dart and `ExoPlayerPlugin` (#1816).
///
/// `exo-buffer-tier` is a synthetic property, not an mpv one: mpv's read-ahead belongs to
/// the mpv.conf editor, so the tier is cached in Dart and only ever crosses the channel as
/// the `bufferTier` argument of `initialize`, where `ExoPlayerCore` resolves it into
/// `LoadControlPolicy.bufferDurations`. It is init-only — losing it is silent, because the
/// native side then keeps the Auto tier and simply never applies the user's choice.
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('an explicit Playback Buffer tier reaches native initialization', () async {
final initialize = await _captureInitialize(
configure: (player) => player.setProperty('exo-buffer-tier', PlaybackBufferTier.extraLarge.nativeValue),
);
final args = initialize.arguments as Map<Object?, Object?>;
expect(args['bufferTier'], 'extra_large');
});
test('no tier write still sends an explicit Auto wire value', () async {
// Native reads the argument with a `?: "auto"` default, but the wire value must be
// present and explicit: a missing key would make a genuine Auto session
// indistinguishable from a tier that was dropped on the way down.
final initialize = await _captureInitialize(configure: (_) async {});
final args = initialize.arguments as Map<Object?, Object?>;
expect(args['bufferTier'], 'auto');
});
}