Skip to content

[video_player] Move iOS/macOS to per-player-instance Pigeon APIs #9529

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

stuartmorgan-g
Copy link
Contributor

Rather than having a single API to talk directly to the plugin, and having most of the methods in that API just do a map lookup and dispatch to a player instance, which was necessary when Pigeon didn't support instantiating multiple instances of an API, have each player instance set up an API instance that the Dart code can talk to directly. This follows the pattern used by plugins that migrated to Pigeon more recently (e.g., google_maps_flutter has a very similar pattern), reduces boilerplate native code, and moves closer to what an FFI-based implementation would look like if we go that route in the future.

Since the Dart unit tests needed to be significantly reworked anyway, this also moves to the pattern we are using in newer plugin code, where we use mockito to mock the Pigeon API surface. The "call log" approach it replaces dates back to pre-Pigeon, when only way to test that the right platform calls were made was to intercept and track method channel calls at the framework level.

Also updates to the latest version of Pigeon.

Pre-Review Checklist

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. 2 3

@stuartmorgan-g
Copy link
Contributor Author

(c.f. #9511, which is the same structural change for the Android implementation.)

[self expectationForPredicate:[NSPredicate predicateWithFormat:@"position == 1234"]
evaluatedWithObject:player
handler:nil];
[self waitForExpectations:@[ positionExpectation ] timeout:3.0];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This expectation was failing for me locally, and I believe it's been flaking occasionally. IIRC this check dates to before we waited for the seek to fully finish (the expectation fulfilled above, whose name I fixed). This test doesn't actually test the logic of seeking itself, only the frame handling around seeking, so expectation above should be all we need to order the test validations correctly.

Same with the change below.

@@ -590,59 +583,36 @@ - (void)testBufferingStateFromPlayer {
}

- (void)testVideoControls {
NSObject<FlutterPluginRegistrar> *registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The rest of the changes in this file are just cleanup for cases that used to test via the plugin, but were really just testing player functionality so could be easily switched to creating a player instance directly instead of creating and driving the player via the plugin (which after my change was creating the player via the plugin, looking it up, and then driving it via the player anyway). See 1963bb3 for that cleanup, isolated as a separate commit.

@end

@implementation FVPTextureBasedVideoPlayer
- (instancetype)initWithAsset:(NSString *)asset
frameUpdater:(FVPFrameUpdater *)frameUpdater
displayLink:(FVPDisplayLink *)displayLink
avFactory:(id<FVPAVFactory>)avFactory
viewProvider:(NSObject<FVPViewProvider> *)viewProvider
onDisposed:(void (^)(int64_t))onDisposed {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This changed to being a post-init property assignment because we need the player ID, which is only available just after creation.

[super dispose];

_onDisposed(self.frameUpdater.textureIdentifier);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This moved to the superclass since all players have an onDisposed now.

/// This method allows you to dispose without touching the event channel. This
/// is useful for the case where the Engine is in the process of deconstruction
/// so the channel is going to die or is already dead.
- (void)disposeSansEventChannel {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is moved from below without change, to create a #pragma mark - separated section for each protocol implemented.

[[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)dispose {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was also moved, for the same reason. The change is the addition of the _onDisposed call.

return FVPCMTimeToMillis([_player currentTime]);
}

- (int64_t)duration {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is moved below as it's not part of of the protocol.

- (void)pausePlayer:(NSInteger)playerIdentifier error:(FlutterError **)error {
FVPVideoPlayer *player = self.playersByIdentifier[@(playerIdentifier)];
[player pause];
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the core of the change: we no longer need boilerplate like this for every player method.

__weak typeof(self) weakSelf = self;
BOOL isTextureBased = textureBasedPlayer != nil;
player.onDisposed = ^() {
SetUpFVPVideoPlayerInstanceApiWithSuffix(messenger, nil, channelSuffix);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The addition of onDisposed to the base class is to allow easily cleaning up the communication channel, without having to push all the communication channel wire-up into the player class (which would be a bigger change, and is also undesirable for potential conversion to using FFI to talk directly to the player instance).

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious why this would have FFI implications? In my mind the communication channel wire-up / tear-down part are implementation details within the platform, the dart side should be able to tell the plugin to create a player, or dispose an existing player without involving any communication channel concepts?

Copy link
Contributor

@LongCatIsLooong LongCatIsLooong left a comment

Choose a reason for hiding this comment

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

LGTM

/// exist.
@visibleForTesting
VideoPlayerInstanceApi ensureApiInitialized(int playerId) {
VideoPlayerInstanceApi? api = _playerApis[playerId];
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: putIfAbsent?

@@ -219,6 +252,14 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform {
return EventChannel('flutter.io/videoPlayer/videoEvents$playerId');
}

VideoPlayerInstanceApi _apiFor(int playerId) {
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong Jul 2, 2025

Choose a reason for hiding this comment

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

Question: as a layman reader I find the "api" in these names slightly confusing. Is this a naming convention? I'd go with something like VideoPlayerInstance _playerWith(playerId) since these are just players?

@@ -219,6 +252,14 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform {
return EventChannel('flutter.io/videoPlayer/videoEvents$playerId');
}

VideoPlayerInstanceApi _apiFor(int playerId) {
final VideoPlayerInstanceApi? api = _playerApis[playerId];
if (api == null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

uber nit:

return _playerApis[playerId] ?? (throw StateError('No active player with ID $playerId.'));

__weak typeof(self) weakSelf = self;
BOOL isTextureBased = textureBasedPlayer != nil;
player.onDisposed = ^() {
SetUpFVPVideoPlayerInstanceApiWithSuffix(messenger, nil, channelSuffix);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious why this would have FFI implications? In my mind the communication channel wire-up / tear-down part are implementation details within the platform, the dart side should be able to tell the plugin to create a player, or dispose an existing player without involving any communication channel concepts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants