diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 3bfde743def7..5e16890885b1 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.5.5 + +* Fixes display of initial frame when paused. + ## 2.5.4 * Fixes new lint warnings. diff --git a/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.m index 654c63fc2b67..70ede124f8e6 100644 --- a/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.m @@ -114,6 +114,10 @@ - (instancetype)initWithURL:(NSURL *)url httpHeaders:(nonnull NSDictionary *)headers avFactory:(id)avFactory registrar:(NSObject *)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; @@ -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 { @@ -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) { @@ -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; } @@ -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; } diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m index bebcb71885fe..fc47da6af7d3 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m @@ -255,6 +255,59 @@ - (void)testSeekToWhilePausedStartsDisplayLinkTemporarily { OCMVerify([mockDisplayLink setRunning:NO]); } +- (void)testInitStartsDisplayLinkTemporarily { + NSObject *mockTextureRegistry = + OCMProtocolMock(@protocol(FlutterTextureRegistry)); + NSObject *registrar = + [GetPluginRegistry() registrarForPlugin:@"InitStartsDisplayLinkTemporarily"]; + NSObject *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 *mockTextureRegistry = OCMProtocolMock(@protocol(FlutterTextureRegistry)); @@ -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]; @@ -318,6 +371,46 @@ - (void)testSeekToWhilePlayingDoesNotStopDisplayLink { OCMVerify(never(), [mockDisplayLink setRunning:NO]); } +- (void)testPauseWhileWaitingForFrameDoesNotStopDisplayLink { + NSObject *mockTextureRegistry = + OCMProtocolMock(@protocol(FlutterTextureRegistry)); + NSObject *registrar = + [GetPluginRegistry() registrarForPlugin:@"PauseWhileWaitingForFrameDoesNotStopDisplayLink"]; + NSObject *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 *registrar = [GetPluginRegistry() registrarForPlugin:@"testDeregistersFromPlayer"]; diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index cef4a092526e..6c6976124875 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -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"