From 23a100afdce7cb57cd2cb9ff9b4cc4af337263fb Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:47:31 +0100 Subject: [PATCH] feat: auto-reconnect VOD playback after network loss --- .../plezy/exoplayer/ExoPlayerPlugin.kt | 29 +++++++++++++++++++ lib/mpv/player/platform/player_android.dart | 4 ++- lib/screens/video_player_screen.dart | 8 +++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt index d8204ebb..a3ef7863 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt @@ -37,6 +37,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, private var configuredTunnelingEnabled: Boolean = true private var debugLoggingEnabled: Boolean = false + private val pendingMpvProperties = mutableListOf>() // FlutterPlugin @@ -133,6 +134,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, } "setSubtitleStyle" -> handleSetSubtitleStyle(call, result) "observeProperty" -> handleObserveProperty(call, result) + "setMpvProperty" -> handleSetMpvProperty(call, result) "setLogLevel" -> { val level = call.argument("level") ?: "warn" debugLoggingEnabled = (level == "v" || level == "debug" || level == "trace") @@ -209,6 +211,9 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, return } + // New media = fresh slate for pending MPV properties + pendingMpvProperties.clear() + activity?.runOnUiThread { if (usingMpvFallback) { // MPV: Build loadfile command with options @@ -472,6 +477,24 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, result.success(null) } + private fun handleSetMpvProperty(call: MethodCall, result: MethodChannel.Result) { + val name = call.argument("name") + val value = call.argument("value") + + if (name == null || value == null) { + result.error("INVALID_ARGS", "Missing 'name' or 'value'", null) + return + } + + if (usingMpvFallback) { + mpvCore?.setProperty(name, value) + } else { + // Store for later application if ExoPlayer falls back to MPV + pendingMpvProperties.add(Pair(name, value)) + } + result.success(null) + } + private fun handleGetStats(result: MethodChannel.Result) { activity?.runOnUiThread { val stats = if (usingMpvFallback) { @@ -644,6 +667,12 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, } } + // Apply any pending MPV properties from Dart + for ((propName, propValue) in pendingMpvProperties) { + mpvCore?.setProperty(propName, propValue) + } + pendingMpvProperties.clear() + // Setup property observers mpvCore?.observeProperty("time-pos", "double") mpvCore?.observeProperty("duration", "double") diff --git a/lib/mpv/player/platform/player_android.dart b/lib/mpv/player/platform/player_android.dart index 8467c66b..d32772e3 100644 --- a/lib/mpv/player/platform/player_android.dart +++ b/lib/mpv/player/platform/player_android.dart @@ -220,7 +220,9 @@ class PlayerAndroid extends PlayerBase { } } break; - // Other properties are no-ops for ExoPlayer + default: + // Forward unknown properties to Kotlin for MPV fallback + await invoke('setMpvProperty', {'name': name, 'value': value}); } } diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 2298dc73..e4a63613 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -1053,6 +1053,14 @@ class VideoPlayerScreenState extends State with WidgetsBindin ? Duration(milliseconds: widget.metadata.viewOffset!) : null; + // Enable FFmpeg auto-reconnect for VOD streams (covers network drops up to 10 min) + if (!widget.isOffline && !widget.isLive) { + await player!.setProperty('stream-lavf-o-append', 'reconnect=1'); + await player!.setProperty('stream-lavf-o-append', 'reconnect_on_network_error=1'); + await player!.setProperty('stream-lavf-o-append', 'reconnect_streamed=1'); + await player!.setProperty('stream-lavf-o-append', 'reconnect_delay_max=600'); + } + // If we have external subtitles, open paused to add them before playback starts. // This prevents a race condition on Android where adding subtitle tracks // during active playback can freeze the video decoder (issue #226).