Repository navigation
Allow cancelling a pending video_player creation #405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| // Copyright 2022-2026 Wang Bin. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'dart:async'; | ||
|
|
||
| import 'package:flutter/services.dart'; | ||
|
|
||
| final Object _createCancellationZoneKey = Object(); | ||
|
|
||
| /// An FVP player creation was cancelled before initialization finished. | ||
| class FvpCreateCancelledException extends PlatformException { | ||
| FvpCreateCancelledException() | ||
| : super( | ||
| code: 'creation cancelled', | ||
| message: 'FVP player creation cancelled'); | ||
| } | ||
|
|
||
| /// Cancels an FVP player creation that is still waiting for MDK preparation. | ||
| /// | ||
| /// Run `VideoPlayerController.initialize()` inside [run] and call [cancel] | ||
| /// before disposing the controller. Without this opt-in scope, disposal still | ||
| /// waits for pending MDK preparation. | ||
| /// | ||
| /// Cancelling completes [run] immediately with [FvpCreateCancelledException]. | ||
| /// Initialization may report the same error later through video_player; await | ||
| /// `controller.dispose()` to wait for native teardown. | ||
| /// Native teardown waits for any texture allocation already in flight. | ||
| /// | ||
| /// ```dart | ||
| /// final cancellation = FvpCreateCancellation(); | ||
| /// Future<void> initialize() async { | ||
| /// try { | ||
| /// await cancellation.run(controller.initialize); | ||
| /// } on FvpCreateCancelledException { | ||
| /// // The controller was replaced or disposed. | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// Future<void> dispose() async { | ||
| /// cancellation.cancel(); | ||
| /// await controller.dispose(); | ||
| /// } | ||
| /// ``` | ||
| class FvpCreateCancellation { | ||
| final Completer<void> _cancelled = Completer<void>(); | ||
|
|
||
| bool get isCancelled => _cancelled.isCompleted; | ||
| Future<void> get whenCancelled => _cancelled.future; | ||
|
|
||
| Future<T> run<T>(Future<T> Function() action) => runZoned( | ||
| () => Future.any<T>([ | ||
| Future.sync(action), | ||
| whenCancelled.then<T>((_) => throw FvpCreateCancelledException()), | ||
| ]), | ||
| zoneValues: {_createCancellationZoneKey: this}, | ||
| ); | ||
|
|
||
| void cancel() { | ||
| if (!_cancelled.isCompleted) _cancelled.complete(); | ||
| } | ||
| } | ||
|
|
||
| FvpCreateCancellation? get currentFvpCreateCancellation => | ||
| Zone.current[_createCancellationZoneKey] as FvpCreateCancellation?; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ import 'package:http/http.dart' as http; | |
| import 'package:path_provider/path_provider.dart'; | ||
| import 'package:ffi/ffi.dart'; | ||
| import 'fvp_platform_interface.dart'; | ||
| import 'create_cancellation.dart'; | ||
| import 'extensions.dart'; | ||
| import 'lib.dart'; | ||
| import 'media_info.dart'; | ||
|
|
@@ -37,10 +38,13 @@ class MdkVideoPlayer extends mdk.Player { | |
| bool _initialized = false; | ||
|
|
||
| @override | ||
| void dispose() { | ||
| streamCtl.close(); | ||
| void dispose() => unawaited(disposeAsync()); | ||
|
|
||
| @override | ||
| Future<void> disposeAsync() async { | ||
| if (!streamCtl.isClosed) streamCtl.close(); | ||
| _initialized = false; | ||
| super.dispose(); | ||
| await super.disposeAsync(); | ||
| } | ||
|
|
||
| MdkVideoPlayer() : super() { | ||
|
|
@@ -116,6 +120,8 @@ class MdkVideoPlayer extends mdk.Player { | |
|
|
||
| class MdkVideoPlayerPlatform extends VideoPlayerPlatform { | ||
| static final _players = <int, MdkVideoPlayer>{}; | ||
| static final _failedEvents = <int, Stream<VideoEvent>>{}; | ||
| static int _nextFailureId = -0x100000000; | ||
| static Map<String, Object>? _globalOpts; | ||
| static Map<String, String>? _playerOpts; | ||
| static int? _maxWidth; | ||
|
|
@@ -268,8 +274,15 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { | |
|
|
||
| @override | ||
| Future<void> dispose(int playerId) async { | ||
| final player = _players.remove(playerId); | ||
| try { | ||
| await player?.disposeAsync(); | ||
| } catch (_) { | ||
| if (player != null) _players[playerId] = player; | ||
| rethrow; | ||
| } | ||
| _platformViewParams.remove(playerId); | ||
| _players.remove(playerId)?.dispose(); | ||
| _failedEvents.remove(playerId); | ||
| } | ||
|
|
||
| /// Creation parameters for players rendering into a platform view | ||
|
|
@@ -287,7 +300,48 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { | |
| Future<int?> create(DataSource dataSource) => | ||
| _create(dataSource, VideoViewType.textureView); | ||
|
|
||
| Future<({bool cancelled, T? value})> _untilCancelled<T>( | ||
| Future<T> operation, FvpCreateCancellation? cancellation) async { | ||
| if (cancellation == null) { | ||
| return (cancelled: false, value: await operation); | ||
| } | ||
| return Future.any<({bool cancelled, T? value})>([ | ||
| operation.then((value) => (cancelled: false, value: value)), | ||
| cancellation.whenCancelled.then((_) => (cancelled: true, value: null)), | ||
| ]); | ||
| } | ||
|
|
||
| int _failedCreate(PlatformException error, | ||
| {MdkVideoPlayer? player, StackTrace? stackTrace}) { | ||
| // video_player requires a non-null message in its event error listener. | ||
| if (error.message == null) { | ||
| error = PlatformException( | ||
| code: error.code, | ||
| message: error.code, | ||
| details: error.details, | ||
| stacktrace: error.stacktrace); | ||
| } | ||
| final id = _nextFailureId--; | ||
| if (player != null) _players[id] = player; | ||
| _failedEvents[id] = Stream<VideoEvent>.error(error, stackTrace); | ||
| return id; | ||
| } | ||
|
|
||
| Future<int> _finishCancelledCreate(MdkVideoPlayer player) async { | ||
| // Finish native teardown before returning the failed ID. If texture release | ||
| // fails, keep the player so the controller can retry through dispose(). | ||
| try { | ||
| await player.disposeAsync(); | ||
| } on PlatformException catch (error, stackTrace) { | ||
| // Keep the player for dispose() to retry instead of rejecting creation: | ||
| // video_player waits for a player ID before it can dispose its controller. | ||
| return _failedCreate(error, player: player, stackTrace: stackTrace); | ||
| } | ||
| return _failedCreate(FvpCreateCancelledException()); | ||
| } | ||
|
|
||
| Future<int?> _create(DataSource dataSource, VideoViewType viewType) async { | ||
| final cancellation = currentFvpCreateCancellation; | ||
| final uri = _toUri(dataSource); | ||
| final player = MdkVideoPlayer(); | ||
| _log.fine('$hashCode player${player.nativeHandle} create($uri)'); | ||
|
|
@@ -333,7 +387,14 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { | |
| player.setProperty('avio.headers', headers); | ||
| } | ||
| player.media = uri; | ||
| int ret = await player.prepare(); // required! | ||
| if (cancellation?.isCancelled ?? false) { | ||
| return _finishCancelledCreate(player); | ||
| } | ||
| final preparation = await _untilCancelled(player.prepare(), cancellation); | ||
| if (preparation.cancelled || (cancellation?.isCancelled ?? false)) { | ||
| return _finishCancelledCreate(player); | ||
| } | ||
| final ret = preparation.value!; | ||
| if (ret < 0) { | ||
| // no throw, handle error in controller.addListener | ||
| _players[-hashCode] = player; | ||
|
|
@@ -344,13 +405,20 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { | |
| //player.dispose(); // dispose for throw | ||
| return -hashCode; | ||
| } | ||
| // Waiting for video geometry can also outlive a cancelled network load. | ||
| final videoGeometry = | ||
| await _untilCancelled(player.textureSize, cancellation); | ||
| if (videoGeometry.cancelled || (cancellation?.isCancelled ?? false)) { | ||
| return _finishCancelledCreate(player); | ||
| } | ||
| final videoSize = videoGeometry.value; | ||
| if (viewType == VideoViewType.platformView) { | ||
| // SurfaceView output (Android): no Flutter texture. The surface is | ||
| // created by the FvpVideoView platform view and attached to the player | ||
| // natively; buffers are sized to the video so TVs with an upscaled UI | ||
| // layer (e.g. 1080p UI on a 4K panel) still scan out at full video | ||
| // resolution. playerId is the native handle instead of a texture id. | ||
| final size = await player.textureSize; | ||
| final size = videoSize; | ||
| if (size == null || size.width <= 0 || size.height <= 0) { | ||
| _players[-hashCode] = player; | ||
| player.streamCtl.addError(PlatformException( | ||
|
|
@@ -404,11 +472,27 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { | |
| } | ||
| // FIXME: pending events will be processed after texture returned, but no events before prepared | ||
| // FIXME: set tunnel too late | ||
| final tex = await player.updateTexture( | ||
| width: _maxWidth, | ||
| height: _maxHeight, | ||
| tunnel: _tunnel, | ||
| fit: _fitMaxSize); | ||
| int tex; | ||
| // Finish texture allocation before disposal so its result can be released | ||
| // before the native player is deleted. | ||
| try { | ||
| tex = await player.updateTexture( | ||
| width: _maxWidth, | ||
| height: _maxHeight, | ||
| tunnel: _tunnel, | ||
| fit: _fitMaxSize); | ||
| } catch (error, stackTrace) { | ||
| if (cancellation?.isCancelled ?? false) { | ||
| return _finishCancelledCreate(player); | ||
| } | ||
| if (error is PlatformException) { | ||
| return _failedCreate(error, player: player, stackTrace: stackTrace); | ||
| } | ||
| rethrow; | ||
| } | ||
| if (cancellation?.isCancelled ?? false) { | ||
| return _finishCancelledCreate(player); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kept this await because Once allocation completes, I release the texture before deleting the player. I tested cancellation with allocation delayed by 0, 20, 200 and 1000 ms on Windows and macOS; all textures were released. |
||
| } | ||
| if (tex < 0) { | ||
| _players[-hashCode] = player; | ||
| player.streamCtl.addError(PlatformException( | ||
|
|
@@ -476,6 +560,10 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { | |
|
|
||
| @override | ||
| Stream<VideoEvent> videoEventsFor(int playerId) { | ||
| final failedEvents = _failedEvents[playerId]; | ||
| if (failedEvents != null) { | ||
| return failedEvents; | ||
| } | ||
| final player = _players[playerId]; | ||
| if (player != null) { | ||
| return player.streamCtl.stream; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
run()completes withStateErroras soon ascancel()fires, whileinitialize()may still be running and later surfaces aPlatformExceptionvia the cancelled event stream.Callers awaiting
run()therefore see a different error thancontroller.value.Consider either:
Future.anythrow) and let initialize fail through the normal platform error path, orinitialize's later error is expected.Also a short usage example in
fvp.dartdartdoc would help, since this is opt-in.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I went with option 2.
run()and the cancellation event now useFvpCreateCancelledException.I kept the early completion because
video_playercan cancel its event subscription duringdispose(), leaving the originalinitialize()future pending. The dartdoc explains the possible later error and that callers should awaitdispose()for native cleanup.