Skip to content

Commit e8fb05f

Browse files
[video_player] Fix initial frame on macOS (#5781)
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
1 parent a5bb26d commit e8fb05f

File tree

4 files changed

+118
-6
lines changed

4 files changed

+118
-6
lines changed

packages/video_player/video_player_avfoundation/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.5.5
2+
3+
* Fixes display of initial frame when paused.
4+
15
## 2.5.4
26

37
* Fixes new lint warnings.

packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.m

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ - (instancetype)initWithURL:(NSURL *)url
114114
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers
115115
avFactory:(id<FVPAVFactory>)avFactory
116116
registrar:(NSObject<FlutterPluginRegistrar> *)registrar;
117+
118+
// Tells the player to run its frame updater until it receives a frame, regardless of the
119+
// play/pause state.
120+
- (void)expectFrame;
117121
@end
118122

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

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

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

527+
- (void)expectFrame {
528+
self.waitingForFrame = YES;
529+
self.displayLink.running = YES;
530+
}
531+
522532
- (void)setIsLooping:(BOOL)isLooping {
523533
_isLooping = isLooping;
524534
}
@@ -710,6 +720,11 @@ - (FVPTextureMessage *)onPlayerSetup:(FVPVideoPlayer *)player
710720
[eventChannel setStreamHandler:player];
711721
player.eventChannel = eventChannel;
712722
self.playersByTextureId[@(textureId)] = player;
723+
724+
// Ensure that the first frame is drawn once available, even if the video isn't played, since
725+
// the engine is now expecting the texture to be populated.
726+
[player expectFrame];
727+
713728
FVPTextureMessage *result = [FVPTextureMessage makeWithTextureId:textureId];
714729
return result;
715730
}

packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,59 @@ - (void)testSeekToWhilePausedStartsDisplayLinkTemporarily {
255255
OCMVerify([mockDisplayLink setRunning:NO]);
256256
}
257257

258+
- (void)testInitStartsDisplayLinkTemporarily {
259+
NSObject<FlutterTextureRegistry> *mockTextureRegistry =
260+
OCMProtocolMock(@protocol(FlutterTextureRegistry));
261+
NSObject<FlutterPluginRegistrar> *registrar =
262+
[GetPluginRegistry() registrarForPlugin:@"InitStartsDisplayLinkTemporarily"];
263+
NSObject<FlutterPluginRegistrar> *partialRegistrar = OCMPartialMock(registrar);
264+
OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry);
265+
FVPDisplayLink *mockDisplayLink =
266+
OCMPartialMock([[FVPDisplayLink alloc] initWithRegistrar:registrar
267+
callback:^(){
268+
}]);
269+
StubFVPDisplayLinkFactory *stubDisplayLinkFactory =
270+
[[StubFVPDisplayLinkFactory alloc] initWithDisplayLink:mockDisplayLink];
271+
AVPlayerItemVideoOutput *mockVideoOutput = OCMPartialMock([[AVPlayerItemVideoOutput alloc] init]);
272+
StubAVPlayer *stubAVPlayer = [[StubAVPlayer alloc] init];
273+
FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc]
274+
initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:stubAVPlayer
275+
output:mockVideoOutput]
276+
displayLinkFactory:stubDisplayLinkFactory
277+
registrar:partialRegistrar];
278+
279+
FlutterError *initalizationError;
280+
[videoPlayerPlugin initialize:&initalizationError];
281+
XCTAssertNil(initalizationError);
282+
FVPCreateMessage *create = [FVPCreateMessage
283+
makeWithAsset:nil
284+
uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8"
285+
packageName:nil
286+
formatHint:nil
287+
httpHeaders:@{}];
288+
FlutterError *createError;
289+
FVPTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&createError];
290+
NSInteger textureId = textureMessage.textureId;
291+
292+
// Init should start the display link temporarily.
293+
OCMVerify([mockDisplayLink setRunning:YES]);
294+
295+
// Simulate a buffer being available.
296+
OCMStub([mockVideoOutput hasNewPixelBufferForItemTime:kCMTimeZero])
297+
.ignoringNonObjectArgs()
298+
.andReturn(YES);
299+
// Any non-zero value is fine here since it won't actually be used, just NULL-checked.
300+
CVPixelBufferRef fakeBufferRef = (CVPixelBufferRef)1;
301+
OCMStub([mockVideoOutput copyPixelBufferForItemTime:kCMTimeZero itemTimeForDisplay:NULL])
302+
.ignoringNonObjectArgs()
303+
.andReturn(fakeBufferRef);
304+
// Simulate a callback from the engine to request a new frame.
305+
FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[@(textureId)];
306+
[player copyPixelBuffer];
307+
// Since a frame was found, and the video is paused, the display link should be paused again.
308+
OCMVerify([mockDisplayLink setRunning:NO]);
309+
}
310+
258311
- (void)testSeekToWhilePlayingDoesNotStopDisplayLink {
259312
NSObject<FlutterTextureRegistry> *mockTextureRegistry =
260313
OCMProtocolMock(@protocol(FlutterTextureRegistry));
@@ -288,8 +341,8 @@ - (void)testSeekToWhilePlayingDoesNotStopDisplayLink {
288341
NSInteger textureId = textureMessage.textureId;
289342

290343
// Ensure that the video is playing before seeking.
291-
FlutterError *pauseError;
292-
[videoPlayerPlugin play:textureMessage error:&pauseError];
344+
FlutterError *playError;
345+
[videoPlayerPlugin play:textureMessage error:&playError];
293346

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

374+
- (void)testPauseWhileWaitingForFrameDoesNotStopDisplayLink {
375+
NSObject<FlutterTextureRegistry> *mockTextureRegistry =
376+
OCMProtocolMock(@protocol(FlutterTextureRegistry));
377+
NSObject<FlutterPluginRegistrar> *registrar =
378+
[GetPluginRegistry() registrarForPlugin:@"PauseWhileWaitingForFrameDoesNotStopDisplayLink"];
379+
NSObject<FlutterPluginRegistrar> *partialRegistrar = OCMPartialMock(registrar);
380+
OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry);
381+
FVPDisplayLink *mockDisplayLink =
382+
OCMPartialMock([[FVPDisplayLink alloc] initWithRegistrar:registrar
383+
callback:^(){
384+
}]);
385+
StubFVPDisplayLinkFactory *stubDisplayLinkFactory =
386+
[[StubFVPDisplayLinkFactory alloc] initWithDisplayLink:mockDisplayLink];
387+
AVPlayerItemVideoOutput *mockVideoOutput = OCMPartialMock([[AVPlayerItemVideoOutput alloc] init]);
388+
FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc]
389+
initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil output:mockVideoOutput]
390+
displayLinkFactory:stubDisplayLinkFactory
391+
registrar:partialRegistrar];
392+
393+
FlutterError *initalizationError;
394+
[videoPlayerPlugin initialize:&initalizationError];
395+
XCTAssertNil(initalizationError);
396+
FVPCreateMessage *create = [FVPCreateMessage
397+
makeWithAsset:nil
398+
uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8"
399+
packageName:nil
400+
formatHint:nil
401+
httpHeaders:@{}];
402+
FlutterError *createError;
403+
FVPTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&createError];
404+
405+
// Run a play/pause cycle to force the pause codepath to run completely.
406+
FlutterError *playPauseError;
407+
[videoPlayerPlugin play:textureMessage error:&playPauseError];
408+
[videoPlayerPlugin pause:textureMessage error:&playPauseError];
409+
410+
// Since a buffer hasn't been available yet, the pause should not have stopped the display link.
411+
OCMVerify(never(), [mockDisplayLink setRunning:NO]);
412+
}
413+
321414
- (void)testDeregistersFromPlayer {
322415
NSObject<FlutterPluginRegistrar> *registrar =
323416
[GetPluginRegistry() registrarForPlugin:@"testDeregistersFromPlayer"];

packages/video_player/video_player_avfoundation/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: video_player_avfoundation
22
description: iOS and macOS implementation of the video_player plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
5-
version: 2.5.4
5+
version: 2.5.5
66

77
environment:
88
sdk: ">=3.2.0 <4.0.0"

0 commit comments

Comments
 (0)