fix(android): include mpv end-file diagnostics

This commit is contained in:
edde746
2026-07-12 08:42:22 +02:00
parent f4ec6ccf20
commit 0135318ded
4 changed files with 159 additions and 2 deletions
@@ -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<String, Any>? {
val reason = event.reason
if (reason == null) {
errorMessage = null
return null
}
val data = mutableMapOf<String, Any>("reason" to reason.id)
if (reason == EndFileReason.Error) {
errorMessage?.let { data["message"] = it }
}
errorMessage = null
return data
}
}
@@ -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)
}
}
@@ -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
}
}
+49
View File
@@ -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<PlayerError>().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<PlayerError>().having((value) => value.message, 'message', 'Playback error')),
);
} finally {
await player.dispose();
}
},
);
});
}