From 3c491be690062842a5bb5640118645cff6432fb8 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:29:16 +0100 Subject: [PATCH] fix: handle mpv end-file errors with global snackbar --- .../com/edde746/plezy/mpv/MpvPlayerCore.kt | 24 ++++++++++++------- lib/main.dart | 8 +++++++ lib/screens/video_player_screen.dart | 19 +++++++++++++-- lib/utils/snackbar_helper.dart | 10 ++++++++ linux/runner/mpv/mpv_player.cc | 2 ++ .../apple/MpvPlayer/MpvPlayerCoreBase.swift | 16 +++++++++++-- windows/runner/mpv/mpv_player.cpp | 2 ++ 7 files changed, 68 insertions(+), 13 deletions(-) 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 cfde1df7..786fa322 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 @@ -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? = 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) } } } } diff --git a/lib/main.dart b/lib/main.dart index 3bc06ea0..74d95c6f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 with WidgetsBindingObserver { themeMode: themeProvider.materialThemeMode, navigatorObservers: [routeObserver, BackKeySuppressorObserver()], home: const OrientationAwareSetup(), + builder: (context, child) => ScaffoldMessenger( + key: rootScaffoldMessengerKey, + child: Scaffold( + backgroundColor: Colors.transparent, + body: child, + ), + ), ), ), ); diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index e4a63613..f33af86b 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -152,6 +152,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin StreamSubscription? _positionSubscription; StreamSubscription? _playbackRestartSubscription; StreamSubscription? _backendSwitchedSubscription; + StreamSubscription? _logSubscription; StreamSubscription? _sleepTimerSubscription; StreamSubscription? _mediaControlsPlayingSubscription; StreamSubscription? _mediaControlsPositionSubscription; @@ -581,6 +582,11 @@ class VideoPlayerScreenState extends State 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 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 with WidgetsBindin _positionSubscription?.cancel(); _playbackRestartSubscription?.cancel(); _backendSwitchedSubscription?.cancel(); + _logSubscription?.cancel(); _sleepTimerSubscription?.cancel(); _mediaControlsPlayingSubscription?.cancel(); _mediaControlsPositionSubscription?.cancel(); @@ -1901,9 +1909,16 @@ class VideoPlayerScreenState extends State 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 diff --git a/lib/utils/snackbar_helper.dart b/lib/utils/snackbar_helper.dart index 67fc53fc..dc880892 100644 --- a/lib/utils/snackbar_helper.dart +++ b/lib/utils/snackbar_helper.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +/// Global key for the root ScaffoldMessenger, allowing snackbars to survive navigation. +final rootScaffoldMessengerKey = GlobalKey(); + /// 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 diff --git a/linux/runner/mpv/mpv_player.cc b/linux/runner/mpv/mpv_player.cc index 88c97b7f..210503cc 100644 --- a/linux/runner/mpv/mpv_player.cc +++ b/linux/runner/mpv/mpv_player.cc @@ -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(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); diff --git a/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift b/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift index 1b1342b9..d517b587 100644 --- a/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift +++ b/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift @@ -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: diff --git a/windows/runner/mpv/mpv_player.cpp b/windows/runner/mpv/mpv_player.cpp index 32dfc334..c9a5b98e 100644 --- a/windows/runner/mpv/mpv_player.cpp +++ b/windows/runner/mpv/mpv_player.cpp @@ -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(end->error)); + data[flutter::EncodableValue("message")] = + flutter::EncodableValue(SanitizeUtf8(mpv_error_string(end->error))); } SendEvent("end-file", data); break;