From 0135318dedf6bfdf4afa2971bfe5e81a4d6ac4ab Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 12 Jul 2026 04:48:53 +0200 Subject: [PATCH] fix(android): include mpv end-file diagnostics --- .../plezy/mpv/MpvEndFileDiagnostics.kt | 35 ++++++++++ .../com/edde746/plezy/mpv/MpvPlayerCore.kt | 7 +- .../edde746/plezy/mpv/MpvPlayerPluginTest.kt | 70 +++++++++++++++++++ test/mpv/player_native_bridge_test.dart | 49 +++++++++++++ 4 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvEndFileDiagnostics.kt diff --git a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvEndFileDiagnostics.kt b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvEndFileDiagnostics.kt new file mode 100644 index 00000000..46930b6a --- /dev/null +++ b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvEndFileDiagnostics.kt @@ -0,0 +1,35 @@ +package com.edde746.plezy.mpv + +import dev.jdtech.mpv.EndFileReason +import dev.jdtech.mpv.LogLevel +import dev.jdtech.mpv.LogMessage +import dev.jdtech.mpv.MpvEvent + +/** Adds the native diagnostic that libmpv-android exposes separately via logFlow. */ +internal class MpvEndFileDiagnostics { + private var errorMessage: String? = null + + fun onStartFile() { + errorMessage = null + } + + fun onLogMessage(message: LogMessage) { + if (message.level == LogLevel.Fatal || message.level == LogLevel.Error) { + errorMessage = message.text.takeIf { it.isNotBlank() } + } + } + + fun onEndFile(event: MpvEvent.EndFile): Map? { + val reason = event.reason + if (reason == null) { + errorMessage = null + return null + } + val data = mutableMapOf("reason" to reason.id) + if (reason == EndFileReason.Error) { + errorMessage?.let { data["message"] = it } + } + errorMessage = null + return data + } +} diff --git a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt index 29b0f615..599ef29f 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt @@ -73,6 +73,7 @@ class MpvPlayerCore( @Volatile private var player: MpvPlayer? = null private var scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private val endFileDiagnostics = MpvEndFileDiagnostics() // mpv writes must stay off the main thread but run in submission order: // setupMpvFallback sets vo/ao/hwdec immediately before the loadfile command, @@ -184,6 +185,7 @@ class MpvPlayerCore( try { disposing = false + endFileDiagnostics.onStartFile() cachedPaused = true pausedForSurfaceLoss = false pendingSurface = null @@ -361,9 +363,9 @@ class MpvPlayerCore( p.eventFlow.collect { event -> when (event) { is MpvEvent.EndFile -> { - val data = event.reason?.let { mapOf("reason" to it.id) } - delegate?.onEvent("end-file", data) + delegate?.onEvent("end-file", endFileDiagnostics.onEndFile(event)) } + is MpvEvent.StartFile -> endFileDiagnostics.onStartFile() is MpvEvent.FileLoaded -> delegate?.onEvent("file-loaded", null) is MpvEvent.PlaybackRestart -> delegate?.onEvent("playback-restart", null) else -> {} @@ -397,6 +399,7 @@ class MpvPlayerCore( private fun collectLogMessages(p: MpvPlayer) { scope.launch(start = CoroutineStart.UNDISPATCHED) { p.logFlow.collect { msg -> + endFileDiagnostics.onLogMessage(msg) emitLog(msg.level.name.lowercase(), msg.prefix, msg.text) } } 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 index faa916dc..6473b8b1 100644 --- a/android/app/src/test/kotlin/com/edde746/plezy/mpv/MpvPlayerPluginTest.kt +++ b/android/app/src/test/kotlin/com/edde746/plezy/mpv/MpvPlayerPluginTest.kt @@ -1,5 +1,10 @@ package com.edde746.plezy.mpv +import dev.jdtech.mpv.EndFileReason +import dev.jdtech.mpv.LogLevel +import dev.jdtech.mpv.LogMessage +import dev.jdtech.mpv.MpvEvent +import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import org.junit.Assert.assertEquals @@ -37,6 +42,59 @@ class MpvPlayerPluginTest { assertNull(result.successValue) } + @Test + fun endFileDiagnosticsPreserveReasonIdAndExposeDependencyErrorLog() { + val diagnostics = MpvEndFileDiagnostics() + diagnostics.onStartFile() + diagnostics.onLogMessage(LogMessage("ffmpeg", LogLevel.Error, "Invalid data found when processing input")) + + assertEquals( + mapOf( + "reason" to 4, + "message" to "Invalid data found when processing input" + ), + diagnostics.onEndFile(MpvEvent.EndFile(EndFileReason.Error)) + ) + } + + @Test + fun endFileDiagnosticsDoNotAttachStaleOrInventedDetails() { + val diagnostics = MpvEndFileDiagnostics() + diagnostics.onLogMessage(LogMessage("ffmpeg", LogLevel.Error, "old failure")) + diagnostics.onStartFile() + + assertEquals(mapOf("reason" to 0), diagnostics.onEndFile(MpvEvent.EndFile(EndFileReason.Eof))) + assertEquals(mapOf("reason" to 4), diagnostics.onEndFile(MpvEvent.EndFile(EndFileReason.Error))) + assertNull(diagnostics.onEndFile(MpvEvent.EndFile(null))) + } + + @Test + fun endFileEventChannelPayloadKeepsExistingEnvelopeAndAddsMessage() { + val sink = RecordingEventSink() + val plugin = MpvPlayerPlugin() + plugin.onListen(null, sink) + + plugin.onEvent( + "end-file", + mapOf( + "reason" to 4, + "message" to "Failed to open stream" + ) + ) + + assertEquals( + mapOf( + "type" to "event", + "name" to "end-file", + "data" to mapOf( + "reason" to 4, + "message" to "Failed to open stream" + ) + ), + sink.successValue + ) + } + private class RecordingResult : MethodChannel.Result { var successValue: Any? = null var errorCode: String? = null @@ -51,4 +109,16 @@ class MpvPlayerPluginTest { override fun notImplemented() = Unit } + + private class RecordingEventSink : EventChannel.EventSink { + var successValue: Any? = null + + override fun success(event: Any?) { + successValue = event + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) = Unit + + override fun endOfStream() = Unit + } } diff --git a/test/mpv/player_native_bridge_test.dart b/test/mpv/player_native_bridge_test.dart index b16a4385..a6415c42 100644 --- a/test/mpv/player_native_bridge_test.dart +++ b/test/mpv/player_native_bridge_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/mpv/models.dart'; import 'package:plezy/mpv/player/player_native.dart'; import 'package:plezy/services/settings_service.dart'; @@ -62,4 +63,52 @@ void main() { }, ); }); + + test('Android mpv end-file error preserves native diagnostic message', () async { + await withMockPlayerChannels( + methodChannelName: 'com.plezy/mpv_player', + eventChannelName: 'com.plezy/mpv_player/events', + testBody: () async { + final player = PlayerNative(); + final error = player.streams.error.first; + try { + player.handlePlayerEvent('end-file', {'reason': 4, 'message': 'Invalid data found when processing input'}); + + await expectLater( + error, + completion( + isA().having( + (value) => value.message, + 'message', + 'Invalid data found when processing input', + ), + ), + ); + } finally { + await player.dispose(); + } + }, + ); + }); + + test('Android mpv legacy end-file error keeps playback error fallback', () async { + await withMockPlayerChannels( + methodChannelName: 'com.plezy/mpv_player', + eventChannelName: 'com.plezy/mpv_player/events', + testBody: () async { + final player = PlayerNative(); + final error = player.streams.error.first; + try { + player.handlePlayerEvent('end-file', {'reason': 4}); + + await expectLater( + error, + completion(isA().having((value) => value.message, 'message', 'Playback error')), + ); + } finally { + await player.dispose(); + } + }, + ); + }); }