diff --git a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt index 91bd881b..f7941701 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt @@ -159,7 +159,7 @@ open class MpvPlayerPlugin( "openContentFd" -> handleOpenContentFd(call, result) "closeContentFd" -> handleCloseContentFd(call, result) "isInitialized" -> result.success(playerCore?.isInitialized ?: false) - "setLogLevel" -> result.success(null) + "setLogLevel" -> handleSetLogLevel(call, result) else -> result.notImplemented() } } @@ -358,14 +358,30 @@ open class MpvPlayerPlugin( val core = playerCore if (core == null) { - result.success(null) + result.error("NOT_INITIALIZED", "Player not initialized", null) return } - core.command(args.toTypedArray()) { - result.success(null) + core.command(args.toTypedArray()) { success -> + if (success) { + result.success(null) + } else { + result.error("COMMAND_FAILED", "mpv command failed", args) + } } } + private fun handleSetLogLevel(call: MethodCall, result: MethodChannel.Result) { + if (call.argument("level") == null) { + result.error("INVALID_ARGS", "Missing 'level'", null) + return + } + result.error( + "UNSUPPORTED", + "Runtime mpv log level changes are not supported on Android", + null + ) + } + private fun handleSetVisible(call: MethodCall, result: MethodChannel.Result) { val visible = call.argument("visible") diff --git a/android/app/src/test/kotlin/com/edde746/plezy/mpv/MpvPlayerPluginTest.kt b/android/app/src/test/kotlin/com/edde746/plezy/mpv/MpvPlayerPluginTest.kt new file mode 100644 index 00000000..faa916dc --- /dev/null +++ b/android/app/src/test/kotlin/com/edde746/plezy/mpv/MpvPlayerPluginTest.kt @@ -0,0 +1,54 @@ +package com.edde746.plezy.mpv + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MpvPlayerPluginTest { + + @Test + fun commandWithoutCoreReportsNotInitialized() { + val result = RecordingResult() + + MpvPlayerPlugin().onMethodCall( + MethodCall("command", mapOf("args" to listOf("seek", "1", "absolute"))), + result + ) + + assertEquals("NOT_INITIALIZED", result.errorCode) + assertNull(result.successValue) + } + + @Test + fun setLogLevelReportsUnsupported() { + val result = RecordingResult() + + MpvPlayerPlugin().onMethodCall( + MethodCall("setLogLevel", mapOf("level" to "warn")), + result + ) + + assertEquals("UNSUPPORTED", result.errorCode) + assertNull(result.successValue) + } + + private class RecordingResult : MethodChannel.Result { + var successValue: Any? = null + var errorCode: String? = null + + override fun success(result: Any?) { + successValue = result + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + this.errorCode = errorCode + } + + override fun notImplemented() = Unit + } +} diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index aa29ffdf..6bf43364 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -879,7 +879,9 @@ class VideoPlayerScreenState extends State with WidgetsBindin _audioFocusFuture!.ignore(); } await currentPlayer.setProperty('msg-level', debugLoggingEnabled ? 'all=debug,ffmpeg/video=warn' : 'all=error'); - await currentPlayer.setLogLevel(debugLoggingEnabled ? 'v' : 'warn'); + if (!Platform.isAndroid || useExoPlayer) { + await currentPlayer.setLogLevel(debugLoggingEnabled ? 'v' : 'warn'); + } await currentPlayer.setProperty('hwdec', _getHwdecValue(enableHardwareDecoding)); await currentPlayer.setProperty( diff --git a/test/mpv/player_native_bridge_test.dart b/test/mpv/player_native_bridge_test.dart new file mode 100644 index 00000000..b16a4385 --- /dev/null +++ b/test/mpv/player_native_bridge_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/mpv/player/player_native.dart'; +import 'package:plezy/services/settings_service.dart'; + +import '../test_helpers/mock_player_channels.dart'; +import '../test_helpers/prefs.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + resetSharedPreferencesForTest(); + SettingsService.resetForTesting(); + await SettingsService.getInstance(); + }); + + test('Android command failure reaches seek recovery', () async { + await withMockPlayerChannels( + methodChannelName: 'com.plezy/mpv_player', + eventChannelName: 'com.plezy/mpv_player/events', + methodHandler: (call) async { + if (call.method == 'initialize') return true; + if (call.method == 'command') { + throw PlatformException(code: 'COMMAND_FAILED', message: 'mpv command failed'); + } + return null; + }, + testBody: () async { + final player = PlayerNative(); + try { + await player.seek(const Duration(seconds: 12)); + expect(player.state.position, Duration.zero); + } finally { + await player.dispose(); + } + }, + ); + }); + + test('Android setLogLevel failure is exposed to Dart', () async { + await withMockPlayerChannels( + methodChannelName: 'com.plezy/mpv_player', + eventChannelName: 'com.plezy/mpv_player/events', + methodHandler: (call) async { + if (call.method == 'initialize') return true; + if (call.method == 'setLogLevel') { + throw PlatformException(code: 'UNSUPPORTED'); + } + return null; + }, + testBody: () async { + final player = PlayerNative(); + try { + await expectLater( + player.setLogLevel('warn'), + throwsA(isA().having((error) => error.code, 'code', 'UNSUPPORTED')), + ); + } finally { + await player.dispose(); + } + }, + ); + }); +}