Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/fvp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import 'src/video_player_mdk.dart'
if (dart.library.html) 'src/video_player_dummy.dart';

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

/// Registers this plugin as the default instance of VideoPlayerPlatform. Then your [VideoPlayer] will support all platforms.
/// If registerWith is not called, the previous(usually official) implementation will be used when available.
Expand Down
65 changes: 65 additions & 0 deletions lib/src/create_cancellation.dart
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},
);

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.


void cancel() {
if (!_cancelled.isCompleted) _cancelled.complete();
}
}

FvpCreateCancellation? get currentFvpCreateCancellation =>
Zone.current[_createCancellationZoneKey] as FvpCreateCancellation?;
13 changes: 10 additions & 3 deletions lib/src/player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,15 @@ class Player {
Libfvp.registerType(nativeHandle, 0, false);
}

/// Release resources
void dispose() async {
/// Release resources.
void dispose() => unawaited(disposeAsync());

/// Release resources and wait for native teardown to finish.
Future<void> disposeAsync() async {
if (_pp == nullptr) {
textureId.dispose();
return;
}
// await: ensure no player ref in fvp plugin before mdkPlayerAPI_delete() in dart
await updateTexture(width: -1);
state = PlaybackState.stopped;
Libfvp.unregisterPort(nativeHandle);
Expand Down Expand Up @@ -204,6 +206,11 @@ class Player {
await FvpPlatform.instance.releaseTexture(nativeHandle, textureId.value!);
textureId.value = null;
}
// Releasing a texture must not wait for media/video size. That future may
// never complete when an in-flight load is cancelled by disposal.
if ((width != null && width <= 0) || (height != null && height <= 0)) {
return -1;
}
Comment thread
JulienDev marked this conversation as resolved.
final size = await _videoSize.future;
if (size == null) {
return -1;
Expand Down
110 changes: 99 additions & 11 deletions lib/src/video_player_mdk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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)');
Expand Down Expand Up @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -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);

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.

}
if (tex < 0) {
_players[-hashCode] = player;
player.streamCtl.addError(PlatformException(
Expand Down Expand Up @@ -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;
Expand Down
Loading