diff --git a/lib/src/controller.dart b/lib/src/controller.dart index 0da6b35..48fc9ed 100644 --- a/lib/src/controller.dart +++ b/lib/src/controller.dart @@ -212,3 +212,85 @@ extension FVPControllerExtensions on VideoPlayerController { _platform.setExternalSubtitle(_getId(this), uri); } } + +/// A [VideoPlayerController] subclass that pre-creates the underlying player so that +/// [appendBuffer] can be called before or during [initialize]. +/// +/// Use this class instead of [VideoPlayerController] when you need to feed media data +/// incrementally (e.g. with a `mdk:` source URL) using [appendBuffer]. +/// +/// Typical usage: +/// ```dart +/// final ctrl = FVPController.network('mdk:'); +/// final initFuture = ctrl.initialize(); // starts but does not await yet +/// ctrl.appendBuffer(chunk1); +/// ctrl.appendBuffer(chunk2, flags: -1); // flags: -1 signals end-of-stream +/// await initFuture; +/// ``` +class FVPController extends VideoPlayerController { + final int _nativeHandle; + + FVPController.network( + String dataSource, { + VideoFormat? formatHint, + Map httpHeaders = const {}, + VideoPlayerOptions? videoPlayerOptions, + Future? closedCaptionFile, + }) : _nativeHandle = _platform.createPendingPlayer(), + super.network(dataSource, + formatHint: formatHint, + httpHeaders: httpHeaders, + videoPlayerOptions: videoPlayerOptions, + closedCaptionFile: closedCaptionFile); + + FVPController.asset( + String dataSource, { + String? package, + Future? closedCaptionFile, + VideoPlayerOptions? videoPlayerOptions, + }) : _nativeHandle = _platform.createPendingPlayer(), + super.asset(dataSource, + package: package, + closedCaptionFile: closedCaptionFile, + videoPlayerOptions: videoPlayerOptions); + + FVPController.file( + File file, { + Future? closedCaptionFile, + VideoPlayerOptions? videoPlayerOptions, + Map httpHeaders = const {}, + }) : _nativeHandle = _platform.createPendingPlayer(), + super.file(file, + closedCaptionFile: closedCaptionFile, + videoPlayerOptions: videoPlayerOptions, + httpHeaders: httpHeaders); + + @override + Future initialize() async { + _platform.setNextPlayerHandle(_nativeHandle); + try { + return await super.initialize(); + } finally { + // Safety: clear the hint if create() was never reached (e.g. on error). + _platform.clearNextPlayerHandle(); + } + } + + @override + Future dispose() async { + // Disposes the pre-created player if initialize() was never called. + _platform.discardPendingPlayer(_nativeHandle); + return super.dispose(); + } + + /// Append media data to the player. Works before, during, and after [initialize]. + /// + /// Used together with a `mdk:` source URL to feed data incrementally. [initialize] + /// will not complete until enough data has been appended via this method. + /// [flags] can be 0 for normal data, or -1 to signal end-of-stream. + /// Returns true on success. + /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#bool-appendbufferconst-uint8_t-data-size_t-size-int-flags + bool appendBuffer(Uint8List data, {int flags = 0}) { + return _platform.appendBufferByHandle(_nativeHandle, data, flags: flags); + } +} diff --git a/lib/src/player.dart b/lib/src/player.dart index 3b89ccb..eb812ca 100644 --- a/lib/src/player.dart +++ b/lib/src/player.dart @@ -496,6 +496,27 @@ class Player { return _seeked!.future; } + /// Append media data to the player. Used with a `mdk:` media source URL to feed data + /// incrementally (similar to MSE). The player will not finish [prepare] until + /// enough data has been appended. + /// [flags] can be 0 for normal data, or -1 to signal end-of-stream. + /// Returns true on success. + /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#bool-appendbufferconst-uint8_t-data-size_t-size-int-flags + bool appendBuffer(Uint8List data, {int flags = 0}) { + final fn = _player.ref.appendBuffer.asFunction< + bool Function(Pointer, Pointer, int, int)>(); + if (data.isEmpty) { + return fn(_player.ref.object, nullptr, 0, flags); + } + // The native appendBuffer copies the data synchronously before returning, + // so the allocated memory can be freed immediately after the call. + final pointer = malloc(data.length); + pointer.asTypedList(data.length).setAll(0, data); + final result = fn(_player.ref.object, pointer, data.length, flags); + malloc.free(pointer); + return result; + } + List bufferedTimeRanges() { const int n = 16; final cbytes = calloc(2 * n); diff --git a/lib/src/video_player_dummy.dart b/lib/src/video_player_dummy.dart index 68dad0d..209c7a0 100644 --- a/lib/src/video_player_dummy.dart +++ b/lib/src/video_player_dummy.dart @@ -69,4 +69,16 @@ class MdkVideoPlayerPlatform { void setExternalVideo(int playerId, String uri) {} void setExternalSubtitle(int playerId, String uri) {} + + int createPendingPlayer() => 0; + + void setNextPlayerHandle(int handle) {} + + void clearNextPlayerHandle() {} + + void discardPendingPlayer(int handle) {} + + bool appendBufferByHandle(int handle, Uint8List data, {int flags = 0}) { + return false; + } } diff --git a/lib/src/video_player_mdk.dart b/lib/src/video_player_mdk.dart index 8e97a77..5a7ec3a 100644 --- a/lib/src/video_player_mdk.dart +++ b/lib/src/video_player_mdk.dart @@ -99,6 +99,12 @@ class MdkVideoPlayer extends mdk.Player { class MdkVideoPlayerPlatform extends VideoPlayerPlatform { static final _players = {}; + // Players indexed by nativeHandle for FVPController to access before textureId is known. + static final _playersByHandle = {}; + // Handles of players that have been promoted into _players (i.e. create() succeeded). + static final _promotedHandles = {}; + // nativeHandle hint set by FVPController.initialize() so create() can reuse the pre-created player. + static int? _nextPlayerHandle; static Map? _globalOpts; static Map? _playerOpts; static int? _maxWidth; @@ -245,13 +251,24 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { @override Future dispose(int playerId) async { - _players.remove(playerId)?.dispose(); + final player = _players.remove(playerId); + if (player != null) { + _promotedHandles.remove(player.nativeHandle); + _playersByHandle.remove(player.nativeHandle); + player.dispose(); + } } @override Future create(DataSource dataSource) async { final uri = _toUri(dataSource); - final player = MdkVideoPlayer(); + // Use a pre-created player if FVPController.initialize() set one up. + final nextHandle = _nextPlayerHandle; + _nextPlayerHandle = null; + final preCreated = nextHandle != null ? _playersByHandle[nextHandle] : null; + final player = preCreated ?? MdkVideoPlayer(); + // Register by nativeHandle so appendBufferByHandle() can find it during prepare(). + _playersByHandle[player.nativeHandle] = player; _log.fine('$hashCode player${player.nativeHandle} create($uri)'); //player.setProperty("keep_open", "1"); @@ -299,6 +316,7 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { if (ret < 0) { // no throw, handle error in controller.addListener _players[-hashCode] = player; + _promotedHandles.add(player.nativeHandle); player.streamCtl.addError(PlatformException( code: 'media open error', message: 'invalid or unsupported media', @@ -315,6 +333,7 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { fit: _fitMaxSize); if (tex < 0) { _players[-hashCode] = player; + _promotedHandles.add(player.nativeHandle); player.streamCtl.addError(PlatformException( code: 'video size error', message: 'invalid or unsupported media with invalid video size', @@ -324,6 +343,7 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { } _log.fine('$hashCode player${player.nativeHandle} textureId/playerId=$tex'); _players[tex] = player; + _promotedHandles.add(player.nativeHandle); return tex; } @@ -506,6 +526,40 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { _players[playerId]?.setMedia(uri, mdk.MediaType.subtitle); } + // FVPController support: create a player before initialize() so appendBuffer() can be called early. + + /// Creates a player and registers it by nativeHandle for [FVPController]. + /// Returns the nativeHandle to use with [setNextPlayerHandle] and [appendBufferByHandle]. + int createPendingPlayer() { + final player = MdkVideoPlayer(); + _playersByHandle[player.nativeHandle] = player; + return player.nativeHandle; + } + + /// Tells [create] to use the pre-created player with [handle] instead of making a new one. + void setNextPlayerHandle(int handle) { + _nextPlayerHandle = handle; + } + + /// Safety: clears the next-player hint if [create] was never called. + void clearNextPlayerHandle() { + _nextPlayerHandle = null; + } + + /// Disposes the pre-created player if [initialize] was never called and it was never promoted. + /// Calling this on an already-promoted or non-existent handle is a safe no-op. + void discardPendingPlayer(int handle) { + if (!_promotedHandles.contains(handle)) { + _playersByHandle.remove(handle)?.dispose(); + } + } + + /// Calls [appendBuffer] on the player identified by [nativeHandle]. + /// Works at any point in the lifecycle: before, during, or after [initialize]. + bool appendBufferByHandle(int handle, Uint8List data, {int flags = 0}) { + return _playersByHandle[handle]?.appendBuffer(data, flags: flags) ?? false; + } + Future _seekToWithFlags( int playerId, Duration position, mdk.SeekFlag flags) async { final player = _players[playerId];