Skip to content
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

[video_player] Fix initial frame on macOS #5781

Merged
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.5.5

* Fixes display of initial frame when paused.

## 2.5.4

* Fixes new lint warnings.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ - (instancetype)initWithURL:(NSURL *)url
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers
avFactory:(id<FVPAVFactory>)avFactory
registrar:(NSObject<FlutterPluginRegistrar> *)registrar;

// Tells the player to run its frame updater until it receives a frame, regardless of the
// play/pause state.
- (void)expectFrame;
@end

static void *timeRangeContext = &timeRangeContext;
Expand Down Expand Up @@ -416,7 +420,9 @@ - (void)updatePlayingState {
} else {
[_player pause];
}
_displayLink.running = _isPlaying;
// If the texture is still waiting for an expected frame, the display link needs to keep
// running until it arrives regardless of the play/pause state.
_displayLink.running = _isPlaying || self.waitingForFrame;
}

- (void)setupEventSinkIfReadyToPlay {
Expand Down Expand Up @@ -509,8 +515,7 @@ - (void)seekTo:(int64_t)location completionHandler:(void (^)(BOOL))completionHan
// must use the display link rather than just informing the engine that a new frame is
// available because the seek completing doesn't guarantee that the pixel buffer is
// already available.
self.waitingForFrame = YES;
self.displayLink.running = YES;
[self expectFrame];
}

if (completionHandler) {
Expand All @@ -519,6 +524,11 @@ - (void)seekTo:(int64_t)location completionHandler:(void (^)(BOOL))completionHan
}];
}

- (void)expectFrame {
self.waitingForFrame = YES;
self.displayLink.running = YES;
}

- (void)setIsLooping:(BOOL)isLooping {
_isLooping = isLooping;
}
Expand Down Expand Up @@ -710,6 +720,11 @@ - (FVPTextureMessage *)onPlayerSetup:(FVPVideoPlayer *)player
[eventChannel setStreamHandler:player];
player.eventChannel = eventChannel;
self.playersByTextureId[@(textureId)] = player;

// Ensure that the first frame is drawn once available, even if the video isn't played, since
// the engine is now expecting the texture to be populated.
[player expectFrame];

FVPTextureMessage *result = [FVPTextureMessage makeWithTextureId:textureId];
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,59 @@ - (void)testSeekToWhilePausedStartsDisplayLinkTemporarily {
OCMVerify([mockDisplayLink setRunning:NO]);
}

- (void)testInitStartsDisplayLinkTemporarily {
NSObject<FlutterTextureRegistry> *mockTextureRegistry =
OCMProtocolMock(@protocol(FlutterTextureRegistry));
NSObject<FlutterPluginRegistrar> *registrar =
[GetPluginRegistry() registrarForPlugin:@"InitStartsDisplayLinkTemporarily"];
NSObject<FlutterPluginRegistrar> *partialRegistrar = OCMPartialMock(registrar);
OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry);
FVPDisplayLink *mockDisplayLink =
OCMPartialMock([[FVPDisplayLink alloc] initWithRegistrar:registrar
callback:^(){
}]);
StubFVPDisplayLinkFactory *stubDisplayLinkFactory =
[[StubFVPDisplayLinkFactory alloc] initWithDisplayLink:mockDisplayLink];
AVPlayerItemVideoOutput *mockVideoOutput = OCMPartialMock([[AVPlayerItemVideoOutput alloc] init]);
StubAVPlayer *stubAVPlayer = [[StubAVPlayer alloc] init];
FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc]
initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:stubAVPlayer
output:mockVideoOutput]
displayLinkFactory:stubDisplayLinkFactory
registrar:partialRegistrar];

FlutterError *initalizationError;
[videoPlayerPlugin initialize:&initalizationError];
XCTAssertNil(initalizationError);
FVPCreateMessage *create = [FVPCreateMessage
makeWithAsset:nil
uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8"
packageName:nil
formatHint:nil
httpHeaders:@{}];
FlutterError *createError;
FVPTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&createError];
NSInteger textureId = textureMessage.textureId;

// Init should start the display link temporarily.
OCMVerify([mockDisplayLink setRunning:YES]);

// Simulate a buffer being available.
OCMStub([mockVideoOutput hasNewPixelBufferForItemTime:kCMTimeZero])
.ignoringNonObjectArgs()
.andReturn(YES);
// Any non-zero value is fine here since it won't actually be used, just NULL-checked.
CVPixelBufferRef fakeBufferRef = (CVPixelBufferRef)1;
OCMStub([mockVideoOutput copyPixelBufferForItemTime:kCMTimeZero itemTimeForDisplay:NULL])
.ignoringNonObjectArgs()
.andReturn(fakeBufferRef);
// Simulate a callback from the engine to request a new frame.
FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[@(textureId)];
[player copyPixelBuffer];
// Since a frame was found, and the video is paused, the display link should be paused again.
OCMVerify([mockDisplayLink setRunning:NO]);
}

- (void)testSeekToWhilePlayingDoesNotStopDisplayLink {
NSObject<FlutterTextureRegistry> *mockTextureRegistry =
OCMProtocolMock(@protocol(FlutterTextureRegistry));
Expand Down Expand Up @@ -288,8 +341,8 @@ - (void)testSeekToWhilePlayingDoesNotStopDisplayLink {
NSInteger textureId = textureMessage.textureId;

// Ensure that the video is playing before seeking.
FlutterError *pauseError;
[videoPlayerPlugin play:textureMessage error:&pauseError];
FlutterError *playError;
[videoPlayerPlugin play:textureMessage error:&playError];

XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"seekTo completes"];
FVPPositionMessage *message = [FVPPositionMessage makeWithTextureId:textureId position:1234];
Expand Down Expand Up @@ -318,6 +371,46 @@ - (void)testSeekToWhilePlayingDoesNotStopDisplayLink {
OCMVerify(never(), [mockDisplayLink setRunning:NO]);
}

- (void)testPauseWhileWaitingForFrameDoesNotStopDisplayLink {
NSObject<FlutterTextureRegistry> *mockTextureRegistry =
OCMProtocolMock(@protocol(FlutterTextureRegistry));
NSObject<FlutterPluginRegistrar> *registrar =
[GetPluginRegistry() registrarForPlugin:@"PauseWhileWaitingForFrameDoesNotStopDisplayLink"];
NSObject<FlutterPluginRegistrar> *partialRegistrar = OCMPartialMock(registrar);
OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry);
FVPDisplayLink *mockDisplayLink =
OCMPartialMock([[FVPDisplayLink alloc] initWithRegistrar:registrar
callback:^(){
}]);
StubFVPDisplayLinkFactory *stubDisplayLinkFactory =
[[StubFVPDisplayLinkFactory alloc] initWithDisplayLink:mockDisplayLink];
AVPlayerItemVideoOutput *mockVideoOutput = OCMPartialMock([[AVPlayerItemVideoOutput alloc] init]);
FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc]
initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil output:mockVideoOutput]
displayLinkFactory:stubDisplayLinkFactory
registrar:partialRegistrar];

FlutterError *initalizationError;
[videoPlayerPlugin initialize:&initalizationError];
XCTAssertNil(initalizationError);
FVPCreateMessage *create = [FVPCreateMessage
makeWithAsset:nil
uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8"
packageName:nil
formatHint:nil
httpHeaders:@{}];
FlutterError *createError;
FVPTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&createError];

// Run a play/pause cycle to force the pause codepath to run completely.
FlutterError *playPauseError;
[videoPlayerPlugin play:textureMessage error:&playPauseError];
[videoPlayerPlugin pause:textureMessage error:&playPauseError];

// Since a buffer hasn't been available yet, the pause should not have stopped the display link.
OCMVerify(never(), [mockDisplayLink setRunning:NO]);
}

- (void)testDeregistersFromPlayer {
NSObject<FlutterPluginRegistrar> *registrar =
[GetPluginRegistry() registrarForPlugin:@"testDeregistersFromPlayer"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: video_player_avfoundation
description: iOS and macOS implementation of the video_player plugin.
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
version: 2.5.4
version: 2.5.5

environment:
sdk: ">=3.2.0 <4.0.0"
Expand Down