fix: race conditions in mpv dispose and track manager, add debug fallback trigger

This commit is contained in:
edde746
2026-03-23 03:48:48 +01:00
parent cc7da5bbc9
commit 9bd2774820
5 changed files with 52 additions and 21 deletions
@@ -1582,6 +1582,12 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
return "Off" return "Off"
} }
fun triggerFallback() {
val uri = currentMediaUri ?: return
val pos = exoPlayer?.currentPosition ?: 0L
delegate?.onFormatUnsupported(uri, currentHeaders, pos, "debug: manual fallback trigger")
}
// Cleanup // Cleanup
fun dispose() { fun dispose() {
@@ -147,6 +147,10 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
playerCore?.debugLoggingEnabled = debugLoggingEnabled playerCore?.debugLoggingEnabled = debugLoggingEnabled
result.success(null) result.success(null)
} }
"triggerFallback" -> {
playerCore?.triggerFallback()
result.success(null)
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@@ -43,6 +43,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
// Audio focus // Audio focus
private var audioFocusManager: AudioFocusManager? = null private var audioFocusManager: AudioFocusManager? = null
@Volatile private var cachedPaused: Boolean = true
private var flutterOverlayApplied = false private var flutterOverlayApplied = false
@@ -71,6 +72,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
try { try {
disposing = false disposing = false
cachedPaused = true
pendingSurface = null pendingSurface = null
// Initialize audio focus handling // Initialize audio focus handling
@@ -89,10 +91,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
catch (e: Exception) { Log.w(TAG, "Failed to resume after focus gain", e) } catch (e: Exception) { Log.w(TAG, "Failed to resume after focus gain", e) }
} }
}, },
isPaused = { isPaused = { cachedPaused }
try { runBlocking { player?.getFlag("pause") == true } }
catch (e: Exception) { true }
}
) )
frameRateManager = FrameRateManager( frameRateManager = FrameRateManager(
activity = activity, activity = activity,
@@ -231,6 +230,9 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
is PropertyChange.Str -> change.value is PropertyChange.Str -> change.value
is PropertyChange.None -> null is PropertyChange.None -> null
} }
if (change.name == "pause" && change is PropertyChange.Flag) {
cachedPaused = change.value
}
delegate?.onPropertyChange(change.name, value) delegate?.onPropertyChange(change.name, value)
} }
} }
@@ -327,7 +329,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
// Public API // Public API
fun setProperty(name: String, value: String) { fun setProperty(name: String, value: String) {
if (!isInitialized) return if (!isInitialized || disposing) return
scope.launch { scope.launch {
try { player?.setProperty(name, value) } try { player?.setProperty(name, value) }
catch (e: Exception) { Log.w(TAG, "setProperty($name) failed", e) } catch (e: Exception) { Log.w(TAG, "setProperty($name) failed", e) }
@@ -335,9 +337,9 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
} }
fun getProperty(name: String): String? { fun getProperty(name: String): String? {
if (!isInitialized) return null if (!isInitialized || disposing) return null
return try { return try {
runBlocking { player?.getString(name) } runBlocking(Dispatchers.IO) { player?.getString(name) }
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@@ -356,7 +358,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
} }
fun command(args: Array<String>) { fun command(args: Array<String>) {
if (!isInitialized || args.isEmpty()) return if (!isInitialized || disposing || args.isEmpty()) return
scope.launch { scope.launch {
try { player?.command(*args) } try { player?.command(*args) }
catch (e: Exception) { Log.w(TAG, "command failed", e) } catch (e: Exception) { Log.w(TAG, "command failed", e) }
@@ -435,7 +437,6 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
// Close player on background thread // Close player on background thread
val p = player val p = player
player = null
if (p != null) { if (p != null) {
Thread { Thread {
try { try {
@@ -443,6 +444,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "MPV close failed", e) Log.w(TAG, "MPV close failed", e)
} }
player = null
Log.d(TAG, "Disposed (native)") Log.d(TAG, "Disposed (native)")
Handler(Looper.getMainLooper()).post { onComplete?.invoke() } Handler(Looper.getMainLooper()).post { onComplete?.invoke() }
}.start() }.start()
+18 -12
View File
@@ -51,6 +51,7 @@ class TrackManager {
bool _isApplyingTrackSelection = false; bool _isApplyingTrackSelection = false;
List<SubtitleTrack> _lastExternalSubtitles = const []; List<SubtitleTrack> _lastExternalSubtitles = const [];
StreamSubscription<Tracks>? _trackLoadingSubscription; StreamSubscription<Tracks>? _trackLoadingSubscription;
Timer? _subtitleFallbackTimer;
/// Cached external subtitles for re-use after backend fallback. /// Cached external subtitles for re-use after backend fallback.
List<SubtitleTrack> get lastExternalSubtitles => _lastExternalSubtitles; List<SubtitleTrack> get lastExternalSubtitles => _lastExternalSubtitles;
@@ -121,7 +122,8 @@ class TrackManager {
} }
// Fallback if playbackRestart doesn't fire // Fallback if playbackRestart doesn't fire
Future.delayed(const Duration(seconds: 3), () { _subtitleFallbackTimer?.cancel();
_subtitleFallbackTimer = Timer(const Duration(seconds: 3), () {
if (waitingForExternalSubsTrackSelection && isActive()) { if (waitingForExternalSubsTrackSelection && isActive()) {
waitingForExternalSubsTrackSelection = false; waitingForExternalSubsTrackSelection = false;
applyTrackSelection(); applyTrackSelection();
@@ -255,13 +257,14 @@ class TrackManager {
/// Handle audio track changes — save stream selection and language preference. /// Handle audio track changes — save stream selection and language preference.
Future<void> onAudioTrackChanged(AudioTrack track) async { Future<void> onAudioTrackChanged(AudioTrack track) async {
final partId = await _guardTrackChange(); final info = mediaInfo;
if (partId == null) return; final partId = await _guardTrackChange(info);
if (partId == null || info == null) return;
int? streamID = _matchTrackByAttributes( int? streamID = _matchTrackByAttributes(
mpvLanguage: track.language, mpvLanguage: track.language,
mpvTitle: track.title, mpvTitle: track.title,
plexTracks: mediaInfo!.audioTracks, plexTracks: info.audioTracks,
getLanguageCode: (t) => t.languageCode, getLanguageCode: (t) => t.languageCode,
getDisplayTitle: (t) => t.displayTitle, getDisplayTitle: (t) => t.displayTitle,
getTitle: (t) => t.title, getTitle: (t) => t.title,
@@ -271,7 +274,7 @@ class TrackManager {
if (streamID != null) { if (streamID != null) {
appLogger.d('Matched audio by lang/title: streamID $streamID'); appLogger.d('Matched audio by lang/title: streamID $streamID');
} else { } else {
final matchedPlex = findPlexTrackForMpvAudio(track, mediaInfo!.audioTracks); final matchedPlex = findPlexTrackForMpvAudio(track, info.audioTracks);
streamID = matchedPlex?.id; streamID = matchedPlex?.id;
if (streamID != null) { if (streamID != null) {
appLogger.d('Matched audio by properties: streamID $streamID'); appLogger.d('Matched audio by properties: streamID $streamID');
@@ -285,7 +288,8 @@ class TrackManager {
/// Handle subtitle track changes — save stream selection and language preference. /// Handle subtitle track changes — save stream selection and language preference.
Future<void> onSubtitleTrackChanged(SubtitleTrack track) async { Future<void> onSubtitleTrackChanged(SubtitleTrack track) async {
final partId = await _guardTrackChange(); final info = mediaInfo;
final partId = await _guardTrackChange(info);
if (partId == null) return; if (partId == null) return;
String? languageCode; String? languageCode;
@@ -295,13 +299,13 @@ class TrackManager {
languageCode = 'none'; languageCode = 'none';
streamID = 0; streamID = 0;
appLogger.i('User turned subtitles off, saving preference'); appLogger.i('User turned subtitles off, saving preference');
} else { } else if (info != null) {
languageCode = track.language; languageCode = track.language;
streamID = _matchTrackByAttributes( streamID = _matchTrackByAttributes(
mpvLanguage: track.language, mpvLanguage: track.language,
mpvTitle: track.title, mpvTitle: track.title,
plexTracks: mediaInfo!.subtitleTracks, plexTracks: info.subtitleTracks,
getLanguageCode: (t) => t.languageCode, getLanguageCode: (t) => t.languageCode,
getDisplayTitle: (t) => t.displayTitle, getDisplayTitle: (t) => t.displayTitle,
getTitle: (t) => t.title, getTitle: (t) => t.title,
@@ -311,7 +315,7 @@ class TrackManager {
if (streamID != null) { if (streamID != null) {
appLogger.d('Matched subtitle by lang/title: streamID $streamID'); appLogger.d('Matched subtitle by lang/title: streamID $streamID');
} else { } else {
final matchedPlex = findPlexTrackForMpvSubtitle(track, mediaInfo!.subtitleTracks); final matchedPlex = findPlexTrackForMpvSubtitle(track, info.subtitleTracks);
streamID = matchedPlex?.id; streamID = matchedPlex?.id;
if (streamID != null) { if (streamID != null) {
appLogger.d('Matched subtitle by properties: streamID $streamID'); appLogger.d('Matched subtitle by properties: streamID $streamID');
@@ -340,16 +344,16 @@ class TrackManager {
} }
/// Common guard checks for track change handlers. /// Common guard checks for track change handlers.
Future<int?> _guardTrackChange() async { Future<int?> _guardTrackChange(PlexMediaInfo? info) async {
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
if (!settings.getRememberTrackSelections()) return null; if (!settings.getRememberTrackSelections()) return null;
if (mediaInfo == null) { if (info == null) {
appLogger.w('No media info available, cannot save stream selection'); appLogger.w('No media info available, cannot save stream selection');
return null; return null;
} }
final partId = mediaInfo!.getPartId(); final partId = info.getPartId();
if (partId == null) { if (partId == null) {
appLogger.w('No part ID available, cannot save stream selection'); appLogger.w('No part ID available, cannot save stream selection');
} }
@@ -439,5 +443,7 @@ class TrackManager {
void dispose() { void dispose() {
_trackLoadingSubscription?.cancel(); _trackLoadingSubscription?.cancel();
_trackLoadingSubscription = null; _trackLoadingSubscription = null;
_subtitleFallbackTimer?.cancel();
_subtitleFallbackTimer = null;
} }
} }
@@ -1,7 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:plezy/widgets/app_icon.dart'; import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@@ -509,6 +511,17 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
), ),
onTap: _togglePerformanceOverlay, onTap: _togglePerformanceOverlay,
), ),
// Debug: Trigger MPV Fallback (Android ExoPlayer only)
if (kDebugMode && Platform.isAndroid && widget.player.playerType == 'exoplayer')
FocusableListTile(
leading: AppIcon(Symbols.swap_horiz_rounded, fill: 1, color: tokens(context).textMuted),
title: const Text('Trigger MPV Fallback'),
onTap: () {
const MethodChannel('com.plezy/exo_player').invokeMethod('triggerFallback');
OverlaySheetController.of(context).close();
},
),
], ],
); );
} }