fix(android): include mpv end-file diagnostics
This commit is contained in:
@@ -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
|
@Volatile private var player: MpvPlayer? = null
|
||||||
private var scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
private var scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
|
private val endFileDiagnostics = MpvEndFileDiagnostics()
|
||||||
|
|
||||||
// mpv writes must stay off the main thread but run in submission order:
|
// mpv writes must stay off the main thread but run in submission order:
|
||||||
// setupMpvFallback sets vo/ao/hwdec immediately before the loadfile command,
|
// setupMpvFallback sets vo/ao/hwdec immediately before the loadfile command,
|
||||||
@@ -184,6 +185,7 @@ class MpvPlayerCore(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
disposing = false
|
disposing = false
|
||||||
|
endFileDiagnostics.onStartFile()
|
||||||
cachedPaused = true
|
cachedPaused = true
|
||||||
pausedForSurfaceLoss = false
|
pausedForSurfaceLoss = false
|
||||||
pendingSurface = null
|
pendingSurface = null
|
||||||
@@ -361,9 +363,9 @@ class MpvPlayerCore(
|
|||||||
p.eventFlow.collect { event ->
|
p.eventFlow.collect { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is MpvEvent.EndFile -> {
|
is MpvEvent.EndFile -> {
|
||||||
val data = event.reason?.let { mapOf("reason" to it.id) }
|
delegate?.onEvent("end-file", endFileDiagnostics.onEndFile(event))
|
||||||
delegate?.onEvent("end-file", data)
|
|
||||||
}
|
}
|
||||||
|
is MpvEvent.StartFile -> endFileDiagnostics.onStartFile()
|
||||||
is MpvEvent.FileLoaded -> delegate?.onEvent("file-loaded", null)
|
is MpvEvent.FileLoaded -> delegate?.onEvent("file-loaded", null)
|
||||||
is MpvEvent.PlaybackRestart -> delegate?.onEvent("playback-restart", null)
|
is MpvEvent.PlaybackRestart -> delegate?.onEvent("playback-restart", null)
|
||||||
else -> {}
|
else -> {}
|
||||||
@@ -397,6 +399,7 @@ class MpvPlayerCore(
|
|||||||
private fun collectLogMessages(p: MpvPlayer) {
|
private fun collectLogMessages(p: MpvPlayer) {
|
||||||
scope.launch(start = CoroutineStart.UNDISPATCHED) {
|
scope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||||
p.logFlow.collect { msg ->
|
p.logFlow.collect { msg ->
|
||||||
|
endFileDiagnostics.onLogMessage(msg)
|
||||||
emitLog(msg.level.name.lowercase(), msg.prefix, msg.text)
|
emitLog(msg.level.name.lowercase(), msg.prefix, msg.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package com.edde746.plezy.mpv
|
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.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
@@ -37,6 +42,59 @@ class MpvPlayerPluginTest {
|
|||||||
assertNull(result.successValue)
|
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 {
|
private class RecordingResult : MethodChannel.Result {
|
||||||
var successValue: Any? = null
|
var successValue: Any? = null
|
||||||
var errorCode: String? = null
|
var errorCode: String? = null
|
||||||
@@ -51,4 +109,16 @@ class MpvPlayerPluginTest {
|
|||||||
|
|
||||||
override fun notImplemented() = Unit
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.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/mpv/player/player_native.dart';
|
||||||
import 'package:plezy/services/settings_service.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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user