From 75ba6b06270f6d7ea95193d1e6ab9bde1010b615 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 13 Jun 2026 13:23:29 +0200 Subject: [PATCH] fix(player): isolate dispose teardown races --- lib/mpv/player/player_base.dart | 20 ++++++++++++++++---- test/mpv/player_open_test.dart | 31 ++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/lib/mpv/player/player_base.dart b/lib/mpv/player/player_base.dart index 3ceb26cd..511de009 100644 --- a/lib/mpv/player/player_base.dart +++ b/lib/mpv/player/player_base.dart @@ -757,11 +757,23 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { if (_disposed) return; _disposed = true; - await _eventSubscription?.cancel(); + try { + await _eventSubscription?.cancel(); + } on PlatformException catch (e, st) { + appLogger.d('Player event stream already detached during dispose', error: e, stackTrace: st); + } on MissingPluginException catch (e, st) { + appLogger.d('Player event stream plugin missing during dispose', error: e, stackTrace: st); + } await _logSubscription?.cancel(); - await methodChannel.invokeMethod('dispose', { - 'preserveDisplayMode': preserveDisplayMode, - }); // Direct call — already guarded by _disposed check above + try { + await methodChannel.invokeMethod('dispose', { + 'preserveDisplayMode': preserveDisplayMode, + }); // Direct call — already guarded by _disposed check above + } on PlatformException catch (e, st) { + appLogger.w('Player native dispose failed during teardown', error: e, stackTrace: st); + } on MissingPluginException catch (e, st) { + appLogger.w('Player native dispose plugin missing during teardown', error: e, stackTrace: st); + } await closeStreamControllers(); } } diff --git a/test/mpv/player_open_test.dart b/test/mpv/player_open_test.dart index 778b1e47..2da8f8db 100644 --- a/test/mpv/player_open_test.dart +++ b/test/mpv/player_open_test.dart @@ -417,6 +417,34 @@ void main() { }, ); }); + + test('dispose continues when native event stream cancellation is already detached', () async { + final calls = []; + + await _withMockChannels( + methodChannelName: 'com.plezy/mpv_player', + eventChannelName: 'com.plezy/mpv_player/events', + methodHandler: (call) { + calls.add(call); + return Future.value(null); + }, + eventHandler: (call) { + if (call.method == 'cancel') { + throw PlatformException(code: 'error', message: 'No active stream to cancel'); + } + return Future.value(null); + }, + testBody: () async { + final player = PlayerNative(); + final playingDone = expectLater(player.streams.playing, emitsDone); + + await expectLater(player.dispose(), completes); + await playingDone; + + expect(calls.where((call) => call.method == 'dispose'), hasLength(1)); + }, + ); + }); }); } @@ -424,6 +452,7 @@ Future _withMockChannels({ required String methodChannelName, required String eventChannelName, Future Function(MethodCall call)? methodHandler, + Future Function(MethodCall call)? eventHandler, required Future Function() testBody, }) async { final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; @@ -449,7 +478,7 @@ Future _withMockChannels({ } }, ); - messenger.setMockMethodCallHandler(eventChannel, (call) async => null); + messenger.setMockMethodCallHandler(eventChannel, eventHandler ?? (call) async => null); try { await testBody();