-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[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
base: main
Are you sure you want to change the base?
[video_player] Move iOS/macOS to per-player-instance Pigeon APIs #9529
Conversation
(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]; |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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); | ||
} |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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]; | ||
} |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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?
There was a problem hiding this 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]; |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
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
[shared_preferences]
pubspec.yaml
with an appropriate new version according to the pub versioning philosophy, or I have commented below to indicate which version change exemption this PR falls under1.CHANGELOG.md
to add a description of the change, following repository CHANGELOG style, or I have commented below to indicate which CHANGELOG exemption this PR falls under1.///
).Footnotes
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