fix: race conditions in mpv dispose and track manager, add debug fallback trigger
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user