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.
68 lines
2.7 KiB
Dart
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');
|
|
});
|
|
}
|