From c2102ec29eb8c02097afcf71df2ee4b70e632138 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 28 Jan 2022 10:33:42 -0800 Subject: [PATCH 1/4] [video_player] Avoid blocking the main thread loading video count --- .../video_player/video_player/CHANGELOG.md | 4 + .../ios/RunnerTests/VideoPlayerTests.m | 91 ++++++++++++++++++- .../ios/Classes/FLTVideoPlayerPlugin.m | 33 ++++++- .../video_player/video_player/pubspec.yaml | 2 +- 4 files changed, 123 insertions(+), 7 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index e28ef83ca720..fc6e28d4495d 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.17 + +* Avoid blocking the main thread loading video count on iOS. + ## 2.2.16 * Introduces `setCaptionOffset` to offset the caption display based on a Duration. diff --git a/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m index 90c7dc2ee95d..dce2891fab73 100644 --- a/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m @@ -8,7 +8,7 @@ #import -@interface FLTVideoPlayer : NSObject +@interface FLTVideoPlayer : NSObject @property(readonly, nonatomic) AVPlayer *player; @end @@ -70,4 +70,93 @@ - (void)testDeregistersFromPlayer { [self waitForExpectationsWithTimeout:1 handler:nil]; } +- (void)testVideoControls { + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = [registry registrarForPlugin:@"TestVideoControls"]; + + FLTVideoPlayerPlugin *videoPlayerPlugin = + (FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar]; + + NSDictionary *videoInitialization = + [self testPlugin:videoPlayerPlugin + uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"]; + XCTAssertEqualObjects(videoInitialization[@"height"], @720); + XCTAssertEqualObjects(videoInitialization[@"width"], @1280); + XCTAssertEqualWithAccuracy([videoInitialization[@"duration"] intValue], 4000, 200); +} + +- (void)testAudioControls { + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = [registry registrarForPlugin:@"TestAudioControls"]; + + FLTVideoPlayerPlugin *videoPlayerPlugin = + (FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar]; + + NSDictionary *audioInitialization = + [self testPlugin:videoPlayerPlugin + uri:@"https://cdn.pixabay.com/audio/2021/09/06/audio_bacd4d6020.mp3"]; + XCTAssertEqualObjects(audioInitialization[@"height"], @0); + XCTAssertEqualObjects(audioInitialization[@"width"], @0); + // Perfect precision not guaranteed. + XCTAssertEqualWithAccuracy([audioInitialization[@"duration"] intValue], 68500, 200); +} + +- (NSDictionary *)testPlugin:(FLTVideoPlayerPlugin *)videoPlayerPlugin + uri:(NSString *)uri { + FlutterError *error; + [videoPlayerPlugin initialize:&error]; + XCTAssertNil(error); + + FLTCreateMessage *create = [[FLTCreateMessage alloc] init]; + create.uri = uri; + FLTTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&error]; + + NSNumber *textureId = textureMessage.textureId; + FLTVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; + XCTAssertNotNil(player); + + XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; + __block NSDictionary *initializationEvent; + [player onListenWithArguments:nil + eventSink:^(NSDictionary *event) { + if ([event[@"event"] isEqualToString:@"initialized"]) { + initializationEvent = event; + XCTAssertEqual(event.count, 4); + [initializedExpectation fulfill]; + } else { + XCTFail(@"Unexpected event: %@", event); + } + }]; + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + // Starts paused. + AVPlayer *avPlayer = player.player; + XCTAssertEqual(avPlayer.rate, 0); + XCTAssertEqual(avPlayer.volume, 1); + XCTAssertEqual(avPlayer.timeControlStatus, AVPlayerTimeControlStatusPaused); + + // Change playback speed. + FLTPlaybackSpeedMessage *playback = [[FLTPlaybackSpeedMessage alloc] init]; + playback.textureId = textureId; + playback.speed = @2; + [videoPlayerPlugin setPlaybackSpeed:playback error:&error]; + XCTAssertNil(error); + XCTAssertEqual(avPlayer.rate, 2); + XCTAssertEqual(avPlayer.timeControlStatus, AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate); + + // Volume + FLTVolumeMessage *volume = [[FLTVolumeMessage alloc] init]; + volume.textureId = textureId; + volume.volume = @(0.1); + [videoPlayerPlugin setVolume:volume error:&error]; + XCTAssertNil(error); + XCTAssertEqual(avPlayer.volume, 0.1f); + + [player onCancelWithArguments:nil]; + + return initializationEvent; +} + @end diff --git a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m index 696fba21f661..d7cb82453ea4 100644 --- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m @@ -331,25 +331,48 @@ - (void)updatePlayingState { - (void)setupEventSinkIfReadyToPlay { if (_eventSink && !_isInitialized) { - BOOL hasVideoTracks = - [[self.player.currentItem.asset tracksWithMediaType:AVMediaTypeVideo] count] != 0; - CGSize size = [self.player currentItem].presentationSize; + AVPlayerItem *currentItem = self.player.currentItem; + CGSize size = currentItem.presentationSize; CGFloat width = size.width; CGFloat height = size.height; + // Wait until tracks are loaded to check duration or if there are any videos. + AVAsset *asset = currentItem.asset; + if ([asset statusOfValueForKey:@"tracks" error:nil] != AVKeyValueStatusLoaded) { + void (^trackCompletionHandler)(void) = ^{ + if ([asset statusOfValueForKey:@"tracks" error:nil] != AVKeyValueStatusLoaded) { + // Cancelled, or something failed. + return; + } + // This completion block will run on an unknown AVFoundation completion + // queue thread. Hop back to the main thread to set up event sink. + if (!NSThread.isMainThread) { + [self performSelector:_cmd onThread:NSThread.mainThread withObject:self waitUntilDone:NO]; + } else { + [self setupEventSinkIfReadyToPlay]; + } + }; + [asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ] + completionHandler:trackCompletionHandler]; + return; + } + + BOOL hasVideoTracks = [asset tracksWithMediaType:AVMediaTypeVideo].count != 0; + // The player has not yet initialized when it contains video tracks. if (hasVideoTracks && height == CGSizeZero.height && width == CGSizeZero.width) { return; } // The player may be initialized but still needs to determine the duration. - if ([self duration] == 0) { + int64_t duration = [self duration]; + if (duration == 0) { return; } _isInitialized = YES; _eventSink(@{ @"event" : @"initialized", - @"duration" : @([self duration]), + @"duration" : @(duration), @"width" : @(width), @"height" : @(height) }); diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 63520f30db4b..9a3697b45b98 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.2.16 +version: 2.2.17 environment: sdk: ">=2.14.0 <3.0.0" From db019eca5e1e737152c0014dfffc58627d959ec3 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 28 Jan 2022 14:17:00 -0800 Subject: [PATCH 2/4] Stop mocking partial --- .../video_player/example/ios/RunnerTests/VideoPlayerTests.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m index dce2891fab73..7131ba0aaaa6 100644 --- a/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m @@ -29,7 +29,7 @@ - (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry { (NSObject *)[[UIApplication sharedApplication] delegate]; NSObject *registrar = [registry registrarForPlugin:@"SeekToInvokestextureFrameAvailable"]; - NSObject *partialRegistrar = OCMPartialMock(registrar); + id partialRegistrar = OCMPartialMock(registrar); OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry); FLTVideoPlayerPlugin *videoPlayerPlugin = (FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:partialRegistrar]; @@ -39,6 +39,7 @@ - (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry { FlutterError *error; [videoPlayerPlugin seekTo:message error:&error]; OCMVerify([mockTextureRegistry textureFrameAvailable:message.textureId.intValue]); + [partialRegistrar stopMocking]; } - (void)testDeregistersFromPlayer { From 04f4686f6239cb5ea61f3cb8e7dc835110b37edb Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 28 Jan 2022 14:23:27 -0800 Subject: [PATCH 3/4] Remove fail --- .../video_player/example/ios/RunnerTests/VideoPlayerTests.m | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m index 7131ba0aaaa6..c57f16672f9d 100644 --- a/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m @@ -29,7 +29,7 @@ - (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry { (NSObject *)[[UIApplication sharedApplication] delegate]; NSObject *registrar = [registry registrarForPlugin:@"SeekToInvokestextureFrameAvailable"]; - id partialRegistrar = OCMPartialMock(registrar); + NSObject *partialRegistrar = OCMPartialMock(registrar); OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry); FLTVideoPlayerPlugin *videoPlayerPlugin = (FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:partialRegistrar]; @@ -39,7 +39,6 @@ - (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry { FlutterError *error; [videoPlayerPlugin seekTo:message error:&error]; OCMVerify([mockTextureRegistry textureFrameAvailable:message.textureId.intValue]); - [partialRegistrar stopMocking]; } - (void)testDeregistersFromPlayer { @@ -126,8 +125,6 @@ - (void)testAudioControls { initializationEvent = event; XCTAssertEqual(event.count, 4); [initializedExpectation fulfill]; - } else { - XCTFail(@"Unexpected event: %@", event); } }]; [self waitForExpectationsWithTimeout:1.0 handler:nil]; From c2325f80c5e4a3b00e6e52964bc1a586605f41aa Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Fri, 28 Jan 2022 16:13:23 -0800 Subject: [PATCH 4/4] Remove main thread check --- .../video_player/ios/Classes/FLTVideoPlayerPlugin.m | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m index d7cb82453ea4..5d09cfed61d2 100644 --- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m @@ -344,13 +344,9 @@ - (void)setupEventSinkIfReadyToPlay { // Cancelled, or something failed. return; } - // This completion block will run on an unknown AVFoundation completion - // queue thread. Hop back to the main thread to set up event sink. - if (!NSThread.isMainThread) { - [self performSelector:_cmd onThread:NSThread.mainThread withObject:self waitUntilDone:NO]; - } else { - [self setupEventSinkIfReadyToPlay]; - } + // This completion block will run on an AVFoundation background queue. + // Hop back to the main thread to set up event sink. + [self performSelector:_cmd onThread:NSThread.mainThread withObject:self waitUntilDone:NO]; }; [asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ] completionHandler:trackCompletionHandler];