fix: handle mpv end-file errors with global snackbar

This commit is contained in:
edde746
2026-03-11 09:29:16 +01:00
parent 580c1d82e5
commit 3c491be690
7 changed files with 68 additions and 13 deletions
@@ -361,15 +361,21 @@ class MpvPlayerCore(private val activity: Activity) :
}
override fun event(eventId: Int) {
val eventName = when (eventId) {
MPVLib.MPV_EVENT_FILE_LOADED -> "file-loaded"
MPVLib.MPV_EVENT_END_FILE -> "end-file"
MPVLib.MPV_EVENT_PLAYBACK_RESTART -> "playback-restart"
else -> null
}
eventName?.let { name ->
activity.runOnUiThread {
delegate?.onEvent(name, null)
when (eventId) {
MPVLib.MPV_EVENT_END_FILE -> {
val eofReached = try { MPVLib.getPropertyBoolean("eof-reached") } catch (_: Exception) { false }
val data: Map<String, Any>? = if (eofReached) {
mapOf("reason" to 0) // EOF
} else {
null // Could be stop, quit, or error — no way to distinguish from JNI
}
activity.runOnUiThread { delegate?.onEvent("end-file", data) }
}
MPVLib.MPV_EVENT_FILE_LOADED -> {
activity.runOnUiThread { delegate?.onEvent("file-loaded", null) }
}
MPVLib.MPV_EVENT_PLAYBACK_RESTART -> {
activity.runOnUiThread { delegate?.onEvent("playback-restart", null) }
}
}
}
+8
View File
@@ -28,6 +28,7 @@ import 'providers/offline_mode_provider.dart';
import 'providers/offline_watch_provider.dart';
import 'providers/companion_remote_provider.dart';
import 'providers/shader_provider.dart';
import 'utils/snackbar_helper.dart';
import 'watch_together/watch_together.dart';
import 'services/multi_server_manager.dart';
import 'services/offline_watch_sync_service.dart';
@@ -408,6 +409,13 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
themeMode: themeProvider.materialThemeMode,
navigatorObservers: [routeObserver, BackKeySuppressorObserver()],
home: const OrientationAwareSetup(),
builder: (context, child) => ScaffoldMessenger(
key: rootScaffoldMessengerKey,
child: Scaffold(
backgroundColor: Colors.transparent,
body: child,
),
),
),
),
);
+17 -2
View File
@@ -152,6 +152,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
StreamSubscription<Duration>? _positionSubscription;
StreamSubscription<void>? _playbackRestartSubscription;
StreamSubscription<void>? _backendSwitchedSubscription;
StreamSubscription<PlayerLog>? _logSubscription;
StreamSubscription<void>? _sleepTimerSubscription;
StreamSubscription<bool>? _mediaControlsPlayingSubscription;
StreamSubscription<Duration>? _mediaControlsPositionSubscription;
@@ -581,6 +582,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Listen to MPV errors
_errorSubscription = player!.streams.error.listen(_onPlayerError);
// Listen to error-level log messages for user-visible snackbars
_logSubscription = player!.streams.log
.where((log) => log.level == PlayerLogLevel.error || log.level == PlayerLogLevel.fatal)
.listen(_onPlayerLogError);
// Listen for backend switched event (ExoPlayer -> MPV fallback on Android)
if (Platform.isAndroid && useExoPlayer) {
_backendSwitchedSubscription = player!.streams.backendSwitched.listen((_) => _onBackendSwitched());
@@ -593,6 +599,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Listen to playback restart to detect first frame ready
_playbackRestartSubscription = player!.streams.playbackRestart.listen((_) async {
_lastLogError = null;
if (!_hasFirstFrame.value) {
_hasFirstFrame.value = true;
Sentry.addBreadcrumb(Breadcrumb(message: 'First frame ready', category: 'player'));
@@ -1742,6 +1749,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
_positionSubscription?.cancel();
_playbackRestartSubscription?.cancel();
_backendSwitchedSubscription?.cancel();
_logSubscription?.cancel();
_sleepTimerSubscription?.cancel();
_mediaControlsPlayingSubscription?.cancel();
_mediaControlsPositionSubscription?.cancel();
@@ -1901,9 +1909,16 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
void _onPlayerError(String error) {
appLogger.e('[Player ERROR] $error');
if (!mounted) return;
if (!mounted || _isExiting.value) return;
showGlobalErrorSnackBar(_lastLogError ?? error);
_handleBackButton();
}
showErrorSnackBar(context, t.messages.failedPlayback(action: 'play', error: error));
String? _lastLogError;
void _onPlayerLogError(PlayerLog log) {
appLogger.e('[Player LOG ERROR] [${log.prefix}] ${log.text}');
_lastLogError = log.text.trim();
}
/// Handle notification when native player switched from ExoPlayer to MPV
+10
View File
@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
/// Global key for the root ScaffoldMessenger, allowing snackbars to survive navigation.
final rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
/// Types of snackbars available in the app
enum SnackBarType {
/// Standard informational snackbar
@@ -51,6 +54,13 @@ void showErrorSnackBar(BuildContext context, String message) {
showSnackBar(context, message, type: SnackBarType.error);
}
/// Shows an error snackbar using the root ScaffoldMessenger (survives navigation).
void showGlobalErrorSnackBar(String message) {
rootScaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red, duration: const Duration(seconds: 4)),
);
}
/// Shows a success snackbar with a message
///
/// [context] The build context
+2
View File
@@ -532,6 +532,8 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
if (end->reason == MPV_END_FILE_REASON_ERROR) {
fl_value_set_string_take(data, "error",
fl_value_new_int(static_cast<int>(end->error)));
fl_value_set_string_take(data, "message",
fl_value_new_string(SanitizeUtf8(mpv_error_string(end->error)).c_str()));
}
SendEvent("end-file", data);
fl_value_unref(data);
+14 -2
View File
@@ -393,8 +393,20 @@ class MpvPlayerCoreBase: NSObject {
}
case MPV_EVENT_END_FILE:
DispatchQueue.main.async {
self.delegate?.onEvent(name: "end-file", data: nil)
if let endFilePtr = event.data?.assumingMemoryBound(to: mpv_event_end_file.self) {
let endFile = endFilePtr.pointee
var data: [String: Any] = ["reason": Int(endFile.reason.rawValue)]
if endFile.reason == MPV_END_FILE_REASON_ERROR {
data["error"] = Int(endFile.error)
data["message"] = safeString(mpv_error_string(endFile.error))
}
DispatchQueue.main.async {
self.delegate?.onEvent(name: "end-file", data: data)
}
} else {
DispatchQueue.main.async {
self.delegate?.onEvent(name: "end-file", data: nil)
}
}
case MPV_EVENT_SHUTDOWN:
+2
View File
@@ -367,6 +367,8 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
if (end->reason == MPV_END_FILE_REASON_ERROR) {
data[flutter::EncodableValue("error")] =
flutter::EncodableValue(static_cast<int>(end->error));
data[flutter::EncodableValue("message")] =
flutter::EncodableValue(SanitizeUtf8(mpv_error_string(end->error)));
}
SendEvent("end-file", data);
break;