fix: player disposed race condition
This commit is contained in:
@@ -55,7 +55,7 @@ class PlayerAndroid extends PlayerBase {
|
||||
if (initialized) return;
|
||||
|
||||
try {
|
||||
final result = await methodChannel.invokeMethod<bool>('initialize', {
|
||||
final result = await invoke<bool>('initialize', {
|
||||
'bufferSizeBytes': _bufferSizeBytes,
|
||||
'tunnelingEnabled': _tunnelingEnabled,
|
||||
});
|
||||
@@ -88,13 +88,13 @@ class PlayerAndroid extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> open(Media media, {bool play = true, bool isLive = false}) async {
|
||||
checkDisposed();
|
||||
if (disposed) return;
|
||||
await _ensureInitialized();
|
||||
|
||||
// Show the video layer
|
||||
await setVisible(true);
|
||||
|
||||
await methodChannel.invokeMethod('open', {
|
||||
await invoke('open', {
|
||||
'uri': media.uri,
|
||||
'headers': media.headers,
|
||||
'startPositionMs': media.start?.inMilliseconds ?? 0,
|
||||
@@ -105,27 +105,23 @@ class PlayerAndroid extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> play() async {
|
||||
checkDisposed();
|
||||
await methodChannel.invokeMethod('play');
|
||||
await invoke('play');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
checkDisposed();
|
||||
await methodChannel.invokeMethod('pause');
|
||||
await invoke('pause');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
checkDisposed();
|
||||
await methodChannel.invokeMethod('stop');
|
||||
await invoke('stop');
|
||||
await setVisible(false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seek(Duration position) async {
|
||||
checkDisposed();
|
||||
await methodChannel.invokeMethod('seek', {'positionMs': position.inMilliseconds});
|
||||
await invoke('seek', {'positionMs': position.inMilliseconds});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -134,20 +130,17 @@ class PlayerAndroid extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> selectAudioTrack(AudioTrack track) async {
|
||||
checkDisposed();
|
||||
await methodChannel.invokeMethod('selectAudioTrack', {'trackId': track.id});
|
||||
await invoke('selectAudioTrack', {'trackId': track.id});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> selectSubtitleTrack(SubtitleTrack track) async {
|
||||
checkDisposed();
|
||||
await methodChannel.invokeMethod('selectSubtitleTrack', {'trackId': track.id});
|
||||
await invoke('selectSubtitleTrack', {'trackId': track.id});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addSubtitleTrack({required String uri, String? title, String? language, bool select = false}) async {
|
||||
checkDisposed();
|
||||
await methodChannel.invokeMethod('addSubtitleTrack', {
|
||||
await invoke('addSubtitleTrack', {
|
||||
'uri': uri,
|
||||
'title': title,
|
||||
'language': language,
|
||||
@@ -161,14 +154,12 @@ class PlayerAndroid extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> setVolume(double volume) async {
|
||||
checkDisposed();
|
||||
await methodChannel.invokeMethod('setVolume', {'volume': volume});
|
||||
await invoke('setVolume', {'volume': volume});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setRate(double rate) async {
|
||||
checkDisposed();
|
||||
await methodChannel.invokeMethod('setRate', {'rate': rate});
|
||||
await invoke('setRate', {'rate': rate});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -177,7 +168,7 @@ class PlayerAndroid extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> setProperty(String name, String value) async {
|
||||
checkDisposed();
|
||||
if (disposed) return;
|
||||
// ExoPlayer doesn't use MPV properties, but we handle common ones
|
||||
switch (name) {
|
||||
case 'pause':
|
||||
@@ -228,7 +219,7 @@ class PlayerAndroid extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<String?> getProperty(String name) async {
|
||||
checkDisposed();
|
||||
if (disposed) return null;
|
||||
// Return state-based values for common properties
|
||||
switch (name) {
|
||||
case 'pause':
|
||||
@@ -260,9 +251,9 @@ class PlayerAndroid extends PlayerBase {
|
||||
/// Get all playback stats from ExoPlayer.
|
||||
/// Returns a map with video/audio codec info, buffer state, and performance metrics.
|
||||
Future<Map<String, dynamic>> getStats() async {
|
||||
checkDisposed();
|
||||
if (disposed) return {};
|
||||
try {
|
||||
final result = await methodChannel.invokeMethod<Map>('getStats');
|
||||
final result = await invoke<Map>('getStats');
|
||||
return Map<String, dynamic>.from(result ?? {});
|
||||
} catch (e) {
|
||||
return {};
|
||||
@@ -282,9 +273,9 @@ class PlayerAndroid extends PlayerBase {
|
||||
|
||||
/// Get the current player type ('exoplayer' or 'mpv' if fallback is active).
|
||||
Future<String> getPlayerType() async {
|
||||
checkDisposed();
|
||||
if (disposed) return 'unknown';
|
||||
try {
|
||||
final result = await methodChannel.invokeMethod<String>('getPlayerType');
|
||||
final result = await invoke<String>('getPlayerType');
|
||||
return result ?? 'unknown';
|
||||
} catch (e) {
|
||||
return 'unknown';
|
||||
@@ -293,7 +284,7 @@ class PlayerAndroid extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> command(List<String> args) async {
|
||||
checkDisposed();
|
||||
if (disposed) return;
|
||||
// Handle MPV commands by translating to ExoPlayer equivalents
|
||||
if (args.isEmpty) return;
|
||||
|
||||
@@ -344,9 +335,8 @@ class PlayerAndroid extends PlayerBase {
|
||||
required int bgOpacity,
|
||||
int subtitlePosition = 100,
|
||||
}) async {
|
||||
checkDisposed();
|
||||
if (!initialized) return;
|
||||
await methodChannel.invokeMethod('setSubtitleStyle', {
|
||||
if (disposed || !initialized) return;
|
||||
await invoke('setSubtitleStyle', {
|
||||
'fontSize': fontSize,
|
||||
'textColor': textColor,
|
||||
'borderSize': borderSize,
|
||||
@@ -363,18 +353,14 @@ class PlayerAndroid extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> setVideoFrameRate(double fps, int durationMs) async {
|
||||
checkDisposed();
|
||||
if (!initialized) return;
|
||||
|
||||
await methodChannel.invokeMethod('setVideoFrameRate', {'fps': fps, 'duration': durationMs});
|
||||
if (disposed || !initialized) return;
|
||||
await invoke('setVideoFrameRate', {'fps': fps, 'duration': durationMs});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearVideoFrameRate() async {
|
||||
checkDisposed();
|
||||
if (!initialized) return;
|
||||
|
||||
await methodChannel.invokeMethod('clearVideoFrameRate');
|
||||
if (disposed || !initialized) return;
|
||||
await invoke('clearVideoFrameRate');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -383,19 +369,15 @@ class PlayerAndroid extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<bool> requestAudioFocus() async {
|
||||
checkDisposed();
|
||||
if (disposed) return false;
|
||||
await _ensureInitialized();
|
||||
|
||||
final result = await methodChannel.invokeMethod<bool>('requestAudioFocus');
|
||||
return result ?? false;
|
||||
return await invoke<bool>('requestAudioFocus') ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> abandonAudioFocus() async {
|
||||
checkDisposed();
|
||||
if (!initialized) return;
|
||||
|
||||
await methodChannel.invokeMethod('abandonAudioFocus');
|
||||
if (disposed || !initialized) return;
|
||||
await invoke('abandonAudioFocus');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -404,8 +386,8 @@ class PlayerAndroid extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> setLogLevel(String level) async {
|
||||
checkDisposed();
|
||||
if (disposed) return;
|
||||
await _ensureInitialized();
|
||||
await methodChannel.invokeMethod('setLogLevel', {'level': level});
|
||||
await invoke('setLogLevel', {'level': level});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../player_native.dart';
|
||||
import '../video_rect_support.dart';
|
||||
|
||||
@@ -9,8 +7,6 @@ import '../video_rect_support.dart';
|
||||
/// The mpv video window is positioned behind the Flutter window,
|
||||
/// with transparent regions allowing the video to show through.
|
||||
class PlayerWindows extends PlayerNative with VideoRectSupport {
|
||||
static const _methodChannel = MethodChannel('com.plezy/mpv_player');
|
||||
|
||||
@override
|
||||
int? get textureId => null; // Uses native window embedding, not Flutter texture
|
||||
|
||||
@@ -22,7 +18,7 @@ class PlayerWindows extends PlayerNative with VideoRectSupport {
|
||||
required int bottom,
|
||||
required double devicePixelRatio,
|
||||
}) async {
|
||||
await _methodChannel.invokeMethod('setVideoRect', {
|
||||
await invoke('setVideoRect', {
|
||||
'left': left,
|
||||
'top': top,
|
||||
'right': right,
|
||||
|
||||
@@ -238,6 +238,9 @@ abstract class Player {
|
||||
// Lifecycle
|
||||
// ============================================
|
||||
|
||||
/// Whether the player has been disposed.
|
||||
bool get disposed;
|
||||
|
||||
/// Dispose of the player and release resources.
|
||||
///
|
||||
/// After calling this, the player instance should not be used.
|
||||
|
||||
@@ -103,7 +103,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
||||
Future<void> observeProperty(String name, String format) async {
|
||||
final propId = _nextPropId++;
|
||||
_propIdToName[propId] = name;
|
||||
await methodChannel.invokeMethod('observeProperty', {'name': name, 'format': format, 'id': propId});
|
||||
await invoke('observeProperty', {'name': name, 'format': format, 'id': propId});
|
||||
}
|
||||
|
||||
void _handleEvent(dynamic event) {
|
||||
@@ -461,11 +461,11 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
||||
_state = update(_state);
|
||||
}
|
||||
|
||||
/// Throws if the player has been disposed.
|
||||
void checkDisposed() {
|
||||
if (_disposed) {
|
||||
throw StateError('Player has been disposed');
|
||||
}
|
||||
/// Safe method channel invocation — no-ops if player is disposed.
|
||||
@protected
|
||||
Future<T?> invoke<T>(String method, [dynamic args]) async {
|
||||
if (_disposed) return null;
|
||||
return methodChannel.invokeMethod<T>(method, args);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -474,7 +474,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
||||
|
||||
@override
|
||||
Future<void> playOrPause() async {
|
||||
checkDisposed();
|
||||
if (_disposed) return;
|
||||
if (_state.playing) {
|
||||
await pause();
|
||||
} else {
|
||||
@@ -484,9 +484,9 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
||||
|
||||
@override
|
||||
Future<bool> setVisible(bool visible) async {
|
||||
checkDisposed();
|
||||
if (_disposed) return false;
|
||||
try {
|
||||
await methodChannel.invokeMethod('setVisible', {'visible': visible});
|
||||
await invoke('setVisible', {'visible': visible});
|
||||
return true;
|
||||
} catch (e) {
|
||||
errorController.add('Failed to set visibility: $e');
|
||||
@@ -546,7 +546,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
||||
|
||||
await _eventSubscription?.cancel();
|
||||
await _logSubscription?.cancel();
|
||||
await methodChannel.invokeMethod('dispose');
|
||||
await methodChannel.invokeMethod('dispose'); // Direct call — already guarded by _disposed check above
|
||||
await closeStreamControllers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class PlayerNative extends PlayerBase {
|
||||
if (initialized) return;
|
||||
|
||||
try {
|
||||
final result = await methodChannel.invokeMethod<Object>('initialize');
|
||||
final result = await invoke<Object>('initialize');
|
||||
if (result is int) {
|
||||
// Linux: initialize returns the texture ID
|
||||
_textureIdValue = result;
|
||||
@@ -104,7 +104,7 @@ class PlayerNative extends PlayerBase {
|
||||
/// Returns null if the call fails.
|
||||
Future<int?> _openContentFd(String contentUri) async {
|
||||
try {
|
||||
return await methodChannel.invokeMethod<int>('openContentFd', {'uri': contentUri});
|
||||
return await invoke<int>('openContentFd', {'uri': contentUri});
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
@@ -112,7 +112,7 @@ class PlayerNative extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> open(Media media, {bool play = true, bool isLive = false}) async {
|
||||
checkDisposed();
|
||||
if (disposed) return;
|
||||
await _ensureInitialized();
|
||||
|
||||
// Show the video layer
|
||||
@@ -153,26 +153,22 @@ class PlayerNative extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> play() async {
|
||||
checkDisposed();
|
||||
await setProperty('pause', 'no');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
checkDisposed();
|
||||
await setProperty('pause', 'yes');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
checkDisposed();
|
||||
await command(['stop']);
|
||||
await methodChannel.invokeMethod('setVisible', {'visible': false});
|
||||
await invoke('setVisible', {'visible': false});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seek(Duration position) async {
|
||||
checkDisposed();
|
||||
await command(['seek', (position.inMilliseconds / 1000.0).toString(), 'absolute']);
|
||||
}
|
||||
|
||||
@@ -182,25 +178,21 @@ class PlayerNative extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> selectAudioTrack(AudioTrack track) async {
|
||||
checkDisposed();
|
||||
await setProperty('aid', track.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> selectSubtitleTrack(SubtitleTrack track) async {
|
||||
checkDisposed();
|
||||
await setProperty('sid', track.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> selectSecondarySubtitleTrack(SubtitleTrack track) async {
|
||||
checkDisposed();
|
||||
await setProperty('secondary-sid', track.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addSubtitleTrack({required String uri, String? title, String? language, bool select = false}) async {
|
||||
checkDisposed();
|
||||
final args = ['sub-add', uri, select ? 'select' : 'auto'];
|
||||
if (title != null) args.add('title=$title');
|
||||
if (language != null) args.add('lang=$language');
|
||||
@@ -213,19 +205,16 @@ class PlayerNative extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> setVolume(double volume) async {
|
||||
checkDisposed();
|
||||
await setProperty('volume', volume.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setRate(double rate) async {
|
||||
checkDisposed();
|
||||
await setProperty('speed', rate.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setAudioDevice(AudioDevice device) async {
|
||||
checkDisposed();
|
||||
await setProperty('audio-device', device.name);
|
||||
}
|
||||
|
||||
@@ -235,23 +224,23 @@ class PlayerNative extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> setProperty(String name, String value) async {
|
||||
checkDisposed();
|
||||
if (disposed) return;
|
||||
await _ensureInitialized();
|
||||
await methodChannel.invokeMethod('setProperty', {'name': name, 'value': value});
|
||||
await invoke('setProperty', {'name': name, 'value': value});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getProperty(String name) async {
|
||||
checkDisposed();
|
||||
if (disposed) return null;
|
||||
await _ensureInitialized();
|
||||
return await methodChannel.invokeMethod<String>('getProperty', {'name': name});
|
||||
return await invoke<String>('getProperty', {'name': name});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> command(List<String> args) async {
|
||||
checkDisposed();
|
||||
if (disposed) return;
|
||||
await _ensureInitialized();
|
||||
await methodChannel.invokeMethod('command', {'args': args});
|
||||
await invoke('command', {'args': args});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -260,9 +249,9 @@ class PlayerNative extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> setLogLevel(String level) async {
|
||||
checkDisposed();
|
||||
if (disposed) return;
|
||||
await _ensureInitialized();
|
||||
await methodChannel.invokeMethod('setLogLevel', {'level': level});
|
||||
await invoke('setLogLevel', {'level': level});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -271,7 +260,6 @@ class PlayerNative extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> setAudioPassthrough(bool enabled) async {
|
||||
checkDisposed();
|
||||
if (enabled) {
|
||||
await setProperty('audio-spdif', 'ac3,eac3,dts,dts-hd,truehd');
|
||||
await setProperty('audio-exclusive', 'yes');
|
||||
@@ -287,47 +275,35 @@ class PlayerNative extends PlayerBase {
|
||||
|
||||
@override
|
||||
Future<void> updateFrame() async {
|
||||
checkDisposed();
|
||||
if (!initialized) return;
|
||||
if (disposed || !initialized) return;
|
||||
if (Platform.isIOS || Platform.isMacOS || Platform.isLinux) {
|
||||
await methodChannel.invokeMethod('updateFrame');
|
||||
await invoke('updateFrame');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setVideoFrameRate(double fps, int durationMs) async {
|
||||
checkDisposed();
|
||||
if (!Platform.isAndroid) return;
|
||||
if (!initialized) return;
|
||||
|
||||
await methodChannel.invokeMethod('setVideoFrameRate', {'fps': fps, 'duration': durationMs});
|
||||
if (!Platform.isAndroid || disposed || !initialized) return;
|
||||
await invoke('setVideoFrameRate', {'fps': fps, 'duration': durationMs});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearVideoFrameRate() async {
|
||||
checkDisposed();
|
||||
if (!Platform.isAndroid) return;
|
||||
if (!initialized) return;
|
||||
|
||||
await methodChannel.invokeMethod('clearVideoFrameRate');
|
||||
if (!Platform.isAndroid || disposed || !initialized) return;
|
||||
await invoke('clearVideoFrameRate');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> requestAudioFocus() async {
|
||||
checkDisposed();
|
||||
if (disposed) return false;
|
||||
if (!Platform.isAndroid) return true;
|
||||
await _ensureInitialized();
|
||||
|
||||
final result = await methodChannel.invokeMethod<bool>('requestAudioFocus');
|
||||
return result ?? false;
|
||||
return await invoke<bool>('requestAudioFocus') ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> abandonAudioFocus() async {
|
||||
checkDisposed();
|
||||
if (!Platform.isAndroid) return;
|
||||
if (!initialized) return;
|
||||
|
||||
await methodChannel.invokeMethod('abandonAudioFocus');
|
||||
if (!Platform.isAndroid || disposed || !initialized) return;
|
||||
await invoke('abandonAudioFocus');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,6 +765,8 @@ class TrackSelectionService {
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (player.disposed) return;
|
||||
|
||||
// Get real tracks (excluding auto and no)
|
||||
final realAudioTracks = player.state.tracks.audio.where((t) => t.id != 'auto' && t.id != 'no').toList();
|
||||
final realSubtitleTracks = player.state.tracks.subtitle.where((t) => t.id != 'auto' && t.id != 'no').toList();
|
||||
|
||||
Reference in New Issue
Block a user