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..c57f16672f9d 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,91 @@ - (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]; + } + }]; + [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..5d09cfed61d2 100644 --- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m @@ -331,25 +331,44 @@ - (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 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]; + 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"