Skip to content

Commit

Permalink
[video_player] Fix initial frame on macOS (#5781)
Browse files Browse the repository at this point in the history
As with seeking while paused, initing a video and not playing it should show the first frame as soon as it is available, but it currently doesn't because the display link isn't running. This uses the same mechanism added for seek to ensure that a video reports a frame to the engine (thus populating the initially-blank textture) as soon as one is available after the player is created, even if it's not played.

Fixes flutter/flutter#140782
  • Loading branch information
stuartmorgan authored Jan 5, 2024
1 parent a5bb26d commit e8fb05f
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 6 deletions.
4 changes: 4 additions & 0 deletions packages/video_player/video_player_avfoundation/CHANGELOG.md
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

0 comments on commit e8fb05f

Please sign in to comment.