fix: handle mpv end-file errors with global snackbar
This commit is contained in:
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user