Skip to content

Allow cancelling a pending video_player creation - #405

Open
JulienDev wants to merge 1 commit into
wang-bin:masterfrom
JulienDev:fix/cancel-pending-create
Open

JulienDev wants to merge 1 commit into
wang-bin:masterfrom
JulienDev:fix/cancel-pending-create

Conversation

@JulienDev

@JulienDev JulienDev commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

If a source stalls in MDK prepare(), createWithOptions() remains pending and VideoPlayerController.dispose() cannot release the player. I reproduced this with a localhost response delayed by 20 seconds on Windows and macOS Release.

This adds an opt-in FvpCreateCancellation scope for initialize(). Calling cancel() before dispose() lets FVP stop and delete the pending player, then return a failed ID so video_player can finish disposal. Cancellation uses FvpCreateCancelledException in both run() and the event stream. The dartdoc includes a usage example. Without the scope, stalled preparation still blocks disposal.

Player.disposeAsync() makes teardown awaitable while keeping void dispose(). MdkVideoPlayer.disposeAsync() closes the event stream and ignores late callbacks. Texture release skips the video-size wait. I kept texture allocation awaited: deleting the player while createTexture() still uses its native handle would be unsafe.

Texture creation and cleanup errors also return a failed ID, avoiding another stuck controller. Platform disposal reports release errors and retains the player for retry while its texture remains registered.

Testing

  • Initial reproduction project: extract it at the root of this PR checkout, then follow example/PENDING_CREATE_REPRO.md. It uses a delayed localhost response and writes MDK logs at Level.ALL. Initial Windows logs at 7b685d6: without cancellation, dispose() timed out after 8 seconds; with cancellation, it completed in 38 ms.
  • Flutter 3.41.9 / video_player 2.11.1: Windows Release with MDK 0.39.0 and macOS Release with MDK 0.38.0 pass cancellation, delayed texture allocation/release, invalid and disconnected sources, and stress (150 rapid switches, 20 rounds of four players, 30 rounds of four cancelled players plus one survivor). Windows stress released 380/380 players and 258/258 textures; Mac stress released 263/263 textures. The four Dart files are identical before and after the rebase onto a233309.
  • Both platforms: injected texture errors are reported without stuck disposal; retrying VideoPlayerPlatform.dispose(id) completes cleanup. Cancellation and normal playback are covered. Holding native texture release confirms that controller disposal waits for it.
  • Windows: 13 format/HTTP/HLS cases pass with default and FFmpeg decoders. DASH fails on this fixture, including on unpatched a233309 with the same MDK 0.39.0 and FFmpeg libraries.
  • 18 Dart tests pass on each platform; analysis reports no issues. Three CMake tests pass on Windows. Mac unit tests use a callbacks.cpp loader shim; Release tests use the real plugin. Windows and macOS CI builds pass on 086395c.

The macOS retest with MDK 0.39.0 is still unconfirmed because I lost access to the test machine. I also reproduced an intermittent parallel-player teardown hang on unpatched fc97343 with the same MDK and native stack offsets. It did not occur in the completed Mac run; I haven't identified its trigger.

@JulienDev
JulienDev force-pushed the fix/cancel-pending-create branch 5 times, most recently from 1c51345 to 7b685d6 Compare September 25, 2026 10:50
@JulienDev
JulienDev marked this pull request as ready for review September 25, 2026 11:11
@wang-bin

Copy link
Copy Markdown
Owner

@cursor review

@cursor

cursor Bot commented Sep 30, 2026

Copy link
Copy Markdown

Bugbot couldn't run — GitHub account mismatch

The GitHub account linked to your Cursor account does not match the PR author.

Please ensure you're using the correct GitHub account, or run Bugbot from a team that covers this repository.

@wang-bin wang-bin left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clear write-up and the Windows repro — the diagnosis matches the video_player create/dispose coupling (dispose waits on _creatingCompleter, which only completes after createWithOptions returns).

The overall approach is good: Zone-scoped opt-in cancellation, awaitable disposeAsync(), skipping _videoSize on texture release, and a failed player id so initialize can finish. CI looks green.

Please address the inline notes before merge. Biggest ones: align MdkVideoPlayer with disposeAsync, and clarify/simplify the Future.any + StateError vs platform error path. Also worth documenting that this remains opt-in (default initialize/dispose still hangs on a stalled prepare()).

Comment thread lib/src/video_player_mdk.dart Outdated
// Resolve video_player's createWithOptions Future only after MDK has been
// stopped and deleted. Its controller can then safely finish dispose().
await player.disposeAsync();
player.streamCtl.close();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disposeAsync() bypasses MdkVideoPlayer.dispose(), so streamCtl has to be closed here by hand.

Prefer closing the stream controller inside an overridden disposeAsync() on MdkVideoPlayer, and keep dispose() => unawaited(disposeAsync()) so both paths tear down the same way:

@override
Future<void> disposeAsync() async {
  if (!streamCtl.isClosed) streamCtl.close();
  _initialized = false;
  await super.disposeAsync();
}

@override
void dispose() => unawaited(disposeAsync());

Then this streamCtl.close() can go away.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, I moved the stream cleanup into MdkVideoPlayer.disposeAsync() and kept dispose() => unawaited(disposeAsync()). I also guard late callbacks, including the textureSize continuation.

(_) => throw StateError('FVP player creation cancelled')),
]),
zoneValues: {_createCancellationZoneKey: this},
);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run() completes with StateError as soon as cancel() fires, while initialize() may still be running and later surfaces a PlatformException via the cancelled event stream.

Callers awaiting run() therefore see a different error than controller.value.

Consider either:

  1. Zone-wrap only (no Future.any throw) and let initialize fail through the normal platform error path, or
  2. Keep the early completion but use one dedicated exception type and document that initialize's later error is expected.

Also a short usage example in fvp.dart dartdoc would help, since this is opt-in.

Copy link
Copy Markdown
Contributor Author

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 use FvpCreateCancelledException.

I kept the early completion because video_player can cancel its event subscription during dispose(), leaving the original initialize() future pending. The dartdoc explains the possible later error and that callers should await dispose() for native cleanup.

Comment thread lib/src/video_player_mdk.dart Outdated
player.streamCtl.close();
final id = _nextFailureId--;
_cancelledEvents[id] = Stream<VideoEvent>.error(PlatformException(
code: 'creation cancelled', message: 'Playback changed'));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message: 'Playback changed' is a bit opaque (not a video_player constant I could find). Prefer something explicit like 'FVP player creation cancelled' so apps that surface errorDescription can tell cancel from a real media failure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to FVP player creation cancelled, using the same FvpCreateCancelledException.

rethrow;
}
if (cancellation?.isCancelled ?? false) {
return _finishCancelledCreate(player);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: prepare / textureSize are raced with _untilCancelled, but updateTexture / createTexture only check isCancelled after the await (and in catch). Cancel during texture creation is unlikely to hang like network prepare, but the handling is inconsistent — wrapping with _untilCancelled would match the earlier waits.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept this await because createTexture() is still using the native player handle. Racing it with cancellation would let _finishCancelledCreate() delete the player before texture creation returns.

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.

Comment thread lib/src/player.dart
Comment thread lib/fvp.dart Outdated
if (dart.library.html) 'src/video_player_dummy.dart';

export 'src/controller.dart';
export 'src/create_cancellation.dart' show FvpCreateCancellation;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a short usage example here (or on FvpCreateCancellation) so callers know this is opt-in and that cancel() must be called before dispose() when initialize may still be pending:

final cancel = FvpCreateCancellation();
try {
  await cancel.run(() => controller.initialize());
} finally {
  cancel.cancel();
  await controller.dispose();
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to the FvpCreateCancellation dartdoc, with cancel() before dispose() and a note that this is opt-in.

@JulienDev
JulienDev force-pushed the fix/cancel-pending-create branch from 7b685d6 to f462191 Compare September 30, 2026 12:08
@JulienDev
JulienDev force-pushed the fix/cancel-pending-create branch from f462191 to 086395c Compare September 30, 2026 12:17
@JulienDev

Copy link
Copy Markdown
Contributor Author

Thanks for the review! I've updated the patch in 086395c and replied inline. I also updated the test results in the PR description.

@JulienDev
JulienDev requested a review from wang-bin September 30, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants