From f4f2571168c0cb53e3934bf166f29f1f9a2d0ebc Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 18 May 2026 20:32:34 +0200 Subject: [PATCH] fix(android): apply dv conversion mode during init --- lib/mpv/player/platform/player_android.dart | 10 ++- lib/screens/video_player_screen.dart | 13 ++-- test/mpv/player_open_test.dart | 84 +++++++++++++++++---- 3 files changed, 84 insertions(+), 23 deletions(-) diff --git a/lib/mpv/player/platform/player_android.dart b/lib/mpv/player/platform/player_android.dart index 2dfe81c5..028c68a9 100644 --- a/lib/mpv/player/platform/player_android.dart +++ b/lib/mpv/player/platform/player_android.dart @@ -195,7 +195,15 @@ class PlayerAndroid extends PlayerBase { break; case 'dv-conversion-mode': _dvConversionMode = value; - if (initialized) await invoke('setDvConversionMode', {'mode': value}); + final initFuture = _initFuture; + if (initialized) { + await invoke('setDvConversionMode', {'mode': value}); + } else if (initFuture != null) { + await initFuture; + if (!disposed && initialized && _dvConversionMode == value) { + await invoke('setDvConversionMode', {'mode': value}); + } + } break; case 'sub-visibility': if (value == 'no') { diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 96a761c2..bdf55c40 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -566,13 +566,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin player = currentPlayer; _playerBackendLabel = currentPlayer.playerType; - // Kick off audio-focus negotiation in parallel with MPV config + prefetch. - // On Android this is a round-trip to AudioManager (~90ms cold). - if (Platform.isAndroid && !widget.isLive) { - _audioFocusFuture = currentPlayer.requestAudioFocus(); - _audioFocusFuture!.ignore(); - } - // Kick off getPlaybackData() in parallel with the rest of MPV setup. // The network/DB work has no dependency on the player — it just needs // the context (providers), which is still safe to touch here because @@ -670,6 +663,12 @@ class VideoPlayerScreenState extends State with WidgetsBindin } } } + // requestAudioFocus initializes Android players, so start it only after + // init-time ExoPlayer options above have been cached. + if (Platform.isAndroid && !widget.isLive) { + _audioFocusFuture = currentPlayer.requestAudioFocus(); + _audioFocusFuture!.ignore(); + } await currentPlayer.setProperty('msg-level', debugLoggingEnabled ? 'all=debug' : 'all=error'); await currentPlayer.setLogLevel(debugLoggingEnabled ? 'v' : 'warn'); await currentPlayer.setProperty('hwdec', _getHwdecValue(enableHardwareDecoding)); diff --git a/test/mpv/player_open_test.dart b/test/mpv/player_open_test.dart index aeede48b..ea803985 100644 --- a/test/mpv/player_open_test.dart +++ b/test/mpv/player_open_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/mpv/mpv.dart'; @@ -32,6 +34,53 @@ void main() { ); }); + test('ExoPlayer applies DV conversion mode changed during in-flight initialization', () async { + final initialize = Completer(); + final calls = []; + + await _withMockChannels( + methodChannelName: 'com.plezy/exo_player', + eventChannelName: 'com.plezy/exo_player/events', + methodHandler: (call) { + calls.add(call); + switch (call.method) { + case 'initialize': + return initialize.future; + case 'requestAudioFocus': + return Future.value(true); + default: + return Future.value(null); + } + }, + testBody: () async { + final player = PlayerAndroid(); + try { + final focusFuture = player.requestAudioFocus(); + await Future.delayed(Duration.zero); + + final modeFuture = player.setProperty('dv-conversion-mode', 'hevc_strip'); + await Future.delayed(Duration.zero); + + final initCall = calls.singleWhere((call) => call.method == 'initialize'); + final initArgs = Map.from(initCall.arguments as Map); + expect(initArgs['dvConversionMode'], 'auto'); + expect(calls.where((call) => call.method == 'setDvConversionMode'), isEmpty); + + initialize.complete(true); + await modeFuture; + await focusFuture; + + final dvCall = calls.singleWhere((call) => call.method == 'setDvConversionMode'); + final dvArgs = Map.from(dvCall.arguments as Map); + expect(dvArgs['mode'], 'hevc_strip'); + } finally { + if (!initialize.isCompleted) initialize.complete(true); + await player.dispose(); + } + }, + ); + }); + test('MPV clears stale Dart track state before opening new media', () async { await _withMockChannels( methodChannelName: 'com.plezy/mpv_player', @@ -61,27 +110,32 @@ void main() { Future _withMockChannels({ required String methodChannelName, required String eventChannelName, + Future Function(MethodCall call)? methodHandler, required Future Function() testBody, }) async { final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; final methodChannel = MethodChannel(methodChannelName); final eventChannel = MethodChannel(eventChannelName); - messenger.setMockMethodCallHandler(methodChannel, (call) async { - switch (call.method) { - case 'initialize': - return true; - case 'observeProperty': - case 'setVisible': - case 'setProperty': - case 'command': - case 'open': - case 'dispose': - return null; - default: - return null; - } - }); + messenger.setMockMethodCallHandler( + methodChannel, + methodHandler ?? + (call) async { + switch (call.method) { + case 'initialize': + return true; + case 'observeProperty': + case 'setVisible': + case 'setProperty': + case 'command': + case 'open': + case 'dispose': + return null; + default: + return null; + } + }, + ); messenger.setMockMethodCallHandler(eventChannel, (call) async => null); try {