[video_player] Avoid blocking the main thread loading video count (#4714)
diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md
index e28ef83..fc6e28d 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 90c7dc2..c57f166 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 <OCMock/OCMock.h>
-@interface FLTVideoPlayer : NSObject
+@interface FLTVideoPlayer : NSObject <FlutterStreamHandler>
@property(readonly, nonatomic) AVPlayer *player;
@end
@@ -70,4 +70,91 @@
[self waitForExpectationsWithTimeout:1 handler:nil];
}
+- (void)testVideoControls {
+ NSObject<FlutterPluginRegistry> *registry =
+ (NSObject<FlutterPluginRegistry> *)[[UIApplication sharedApplication] delegate];
+ NSObject<FlutterPluginRegistrar> *registrar = [registry registrarForPlugin:@"TestVideoControls"];
+
+ FLTVideoPlayerPlugin *videoPlayerPlugin =
+ (FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar];
+
+ NSDictionary<NSString *, id> *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<FlutterPluginRegistry> *registry =
+ (NSObject<FlutterPluginRegistry> *)[[UIApplication sharedApplication] delegate];
+ NSObject<FlutterPluginRegistrar> *registrar = [registry registrarForPlugin:@"TestAudioControls"];
+
+ FLTVideoPlayerPlugin *videoPlayerPlugin =
+ (FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar];
+
+ NSDictionary<NSString *, id> *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<NSString *, id> *)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<NSString *, id> *initializationEvent;
+ [player onListenWithArguments:nil
+ eventSink:^(NSDictionary<NSString *, id> *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 696fba2..5d09cfe 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)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 63520f3..9a3697b 100644
--- a/packages/video_player/video_player/pubspec.yaml
+++ b/packages/video_player/video_player/pubspec.yaml
@@ -3,7 +3,7 @@
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"