Skip to content
Draft
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
82 changes: 82 additions & 0 deletions lib/src/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> httpHeaders = const {},
VideoPlayerOptions? videoPlayerOptions,
Future<ClosedCaptionFile>? closedCaptionFile,
}) : _nativeHandle = _platform.createPendingPlayer(),
super.network(dataSource,
formatHint: formatHint,
httpHeaders: httpHeaders,
videoPlayerOptions: videoPlayerOptions,
closedCaptionFile: closedCaptionFile);

FVPController.asset(
String dataSource, {
String? package,
Future<ClosedCaptionFile>? closedCaptionFile,
VideoPlayerOptions? videoPlayerOptions,
}) : _nativeHandle = _platform.createPendingPlayer(),
super.asset(dataSource,
package: package,
closedCaptionFile: closedCaptionFile,
videoPlayerOptions: videoPlayerOptions);

FVPController.file(
File file, {
Future<ClosedCaptionFile>? closedCaptionFile,
VideoPlayerOptions? videoPlayerOptions,
Map<String, String> httpHeaders = const {},
}) : _nativeHandle = _platform.createPendingPlayer(),
super.file(file,
closedCaptionFile: closedCaptionFile,
videoPlayerOptions: videoPlayerOptions,
httpHeaders: httpHeaders);

@override
Future<void> 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<void> 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);
}
}
21 changes: 21 additions & 0 deletions lib/src/player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<mdkPlayer>, Pointer<Uint8>, 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<Uint8>(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<DurationRange> bufferedTimeRanges() {
const int n = 16;
final cbytes = calloc<Int64>(2 * n);
Expand Down
12 changes: 12 additions & 0 deletions lib/src/video_player_dummy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
58 changes: 56 additions & 2 deletions lib/src/video_player_mdk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ class MdkVideoPlayer extends mdk.Player {

class MdkVideoPlayerPlatform extends VideoPlayerPlatform {
static final _players = <int, MdkVideoPlayer>{};
// Players indexed by nativeHandle for FVPController to access before textureId is known.
static final _playersByHandle = <int, MdkVideoPlayer>{};
// Handles of players that have been promoted into _players (i.e. create() succeeded).
static final _promotedHandles = <int>{};
// nativeHandle hint set by FVPController.initialize() so create() can reuse the pre-created player.
static int? _nextPlayerHandle;
static Map<String, Object>? _globalOpts;
static Map<String, String>? _playerOpts;
static int? _maxWidth;
Expand Down Expand Up @@ -245,13 +251,24 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform {

@override
Future<void> 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<int?> 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");
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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;
}

Expand Down Expand Up @@ -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<void> _seekToWithFlags(
int playerId, Duration position, mdk.SeekFlag flags) async {
final player = _players[playerId];
Expand Down
Loading