Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
[video_player] Fix initial frame on macOS
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-g committed Jan 2, 2024
commit 68f450d02b4c6c99d5326b914513fc23384bb9c1
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.5.4

* Fixes display of initial frame when paused.

## 2.5.3

* Publishes an instance of the plugin to the registrar on macOS, as on iOS.
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,7 @@ - (void)updatePlayingState {
} else {
[_player pause];
}
_displayLink.running = _isPlaying;
_displayLink.running = _isPlaying || self.waitingForFrame;
}

- (void)setupEventSinkIfReadyToPlay {
Expand Down Expand Up @@ -509,8 +513,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 +522,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 +718,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,58 @@ - (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 +340,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 +370,46 @@ - (void)testSeekToWhilePlayingDoesNotStopDisplayLink {
OCMVerify(never(), [mockDisplayLink setRunning:NO]);
}

- (void)testPauseWhileWaitingForFrameDoesNotStopDisplayLink {
NSObject<FlutterTextureRegistry> *mockTextureRegistry =
OCMProtocolMock(@protocol(FlutterTextureRegistry));
NSObject<FlutterPluginRegistrar> *registrar =
[GetPluginRegistry() registrarForPlugin:@"SeekToWhilePausedStartsDisplayLinkTemporarily"];
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.3
version: 2.5.4

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