diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index c4425521fa95..4c1b9a54fd3f 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.6 + +* Fixes hang when seeking to end of video. + ## 2.4.5 * Updates functions without a prototype to avoid deprecation warning. diff --git a/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart b/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart index ae3cd7e3ea89..d8a73b09d24a 100644 --- a/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart @@ -71,13 +71,27 @@ void main() { expect(await controller.position, greaterThan(Duration.zero)); }); - testWidgets('can seek', (WidgetTester tester) async { - await controller.initialize(); + testWidgets( + 'can seek', + (WidgetTester tester) async { + await controller.initialize(); - await controller.seekTo(const Duration(seconds: 3)); + await controller.seekTo(const Duration(seconds: 3)); - expect(await controller.position, const Duration(seconds: 3)); - }); + expect(controller.value.position, const Duration(seconds: 3)); + }, + ); + + testWidgets( + 'can seek to end', + (WidgetTester tester) async { + await controller.initialize(); + + await controller.seekTo(controller.value.duration); + + expect(controller.value.duration, controller.value.position); + }, + ); testWidgets('can be paused', (WidgetTester tester) async { await controller.initialize(); diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m index 1ec18e762ae4..a9d7eac073df 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m @@ -8,6 +8,7 @@ #import #import +#import @interface FLTVideoPlayer : NSObject @property(readonly, nonatomic) AVPlayer *player; @@ -61,6 +62,46 @@ - (CGAffineTransform)preferredTransform { @interface VideoPlayerTests : XCTestCase @end +@interface StubAVPlayer : AVPlayer +@property(readonly, nonatomic) NSNumber *beforeTolerance; +@property(readonly, nonatomic) NSNumber *afterTolerance; +@end + +@implementation StubAVPlayer + +- (void)seekToTime:(CMTime)time + toleranceBefore:(CMTime)toleranceBefore + toleranceAfter:(CMTime)toleranceAfter + completionHandler:(void (^)(BOOL finished))completionHandler { + _beforeTolerance = [NSNumber numberWithLong:toleranceBefore.value]; + _afterTolerance = [NSNumber numberWithLong:toleranceAfter.value]; + completionHandler(YES); +} + +@end + +@interface StubFVPPlayerFactory : NSObject + +@property(nonatomic, strong) StubAVPlayer *stubAVPlayer; + +- (instancetype)initWithPlayer:(StubAVPlayer *)stubAVPlayer; + +@end + +@implementation StubFVPPlayerFactory + +- (instancetype)initWithPlayer:(StubAVPlayer *)stubAVPlayer { + self = [super init]; + _stubAVPlayer = stubAVPlayer; + return self; +} + +- (AVPlayer *)playerWithPlayerItem:(AVPlayerItem *)playerItem { + return _stubAVPlayer; +} + +@end + @implementation VideoPlayerTests - (void)testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream { @@ -226,6 +267,81 @@ - (void)testTransformFix { [self validateTransformFixForOrientation:UIImageOrientationRightMirrored]; } +- (void)testSeekToleranceWhenNotSeekingToEnd { + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = [registry registrarForPlugin:@"TestSeekTolerance"]; + + StubAVPlayer *stubAVPlayer = [[StubAVPlayer alloc] init]; + StubFVPPlayerFactory *stubFVPPlayerFactory = + [[StubFVPPlayerFactory alloc] initWithPlayer:stubAVPlayer]; + FLTVideoPlayerPlugin *pluginWithMockAVPlayer = + [[FLTVideoPlayerPlugin alloc] initWithPlayerFactory:stubFVPPlayerFactory registrar:registrar]; + + FlutterError *error; + [pluginWithMockAVPlayer initialize:&error]; + XCTAssertNil(error); + + FLTCreateMessage *create = [FLTCreateMessage + makeWithAsset:nil + uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" + packageName:nil + formatHint:nil + httpHeaders:@{}]; + FLTTextureMessage *textureMessage = [pluginWithMockAVPlayer create:create error:&error]; + NSNumber *textureId = textureMessage.textureId; + + XCTestExpectation *initializedExpectation = + [self expectationWithDescription:@"seekTo has zero tolerance when seeking not to end"]; + FLTPositionMessage *message = [FLTPositionMessage makeWithTextureId:textureId position:@1234]; + [pluginWithMockAVPlayer seekTo:message + completion:^(FlutterError *_Nullable error) { + [initializedExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + XCTAssertEqual([stubAVPlayer.beforeTolerance intValue], 0); + XCTAssertEqual([stubAVPlayer.afterTolerance intValue], 0); +} + +- (void)testSeekToleranceWhenSeekingToEnd { + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = + [registry registrarForPlugin:@"TestSeekToEndTolerance"]; + + StubAVPlayer *stubAVPlayer = [[StubAVPlayer alloc] init]; + StubFVPPlayerFactory *stubFVPPlayerFactory = + [[StubFVPPlayerFactory alloc] initWithPlayer:stubAVPlayer]; + FLTVideoPlayerPlugin *pluginWithMockAVPlayer = + [[FLTVideoPlayerPlugin alloc] initWithPlayerFactory:stubFVPPlayerFactory registrar:registrar]; + + FlutterError *error; + [pluginWithMockAVPlayer initialize:&error]; + XCTAssertNil(error); + + FLTCreateMessage *create = [FLTCreateMessage + makeWithAsset:nil + uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" + packageName:nil + formatHint:nil + httpHeaders:@{}]; + FLTTextureMessage *textureMessage = [pluginWithMockAVPlayer create:create error:&error]; + NSNumber *textureId = textureMessage.textureId; + + XCTestExpectation *initializedExpectation = + [self expectationWithDescription:@"seekTo has non-zero tolerance when seeking to end"]; + // The duration of this video is "0" due to the non standard initiliatazion process. + FLTPositionMessage *message = [FLTPositionMessage makeWithTextureId:textureId position:@0]; + [pluginWithMockAVPlayer seekTo:message + completion:^(FlutterError *_Nullable error) { + [initializedExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + XCTAssertGreaterThan([stubAVPlayer.beforeTolerance intValue], 0); + XCTAssertGreaterThan([stubAVPlayer.afterTolerance intValue], 0); +} + - (NSDictionary *)testPlugin:(FLTVideoPlayerPlugin *)videoPlayerPlugin uri:(NSString *)uri { FlutterError *error; diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m index 6c39cb9a4a77..ccece1127699 100644 --- a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m @@ -3,6 +3,7 @@ // found in the LICENSE file. #import "FLTVideoPlayerPlugin.h" +#import "FLTVideoPlayerPlugin_Test.h" #import #import @@ -33,6 +34,16 @@ - (void)onDisplayLink:(CADisplayLink *)link { } @end +@interface FVPDefaultPlayerFactory : NSObject +@end + +@implementation FVPDefaultPlayerFactory +- (AVPlayer *)playerWithPlayerItem:(AVPlayerItem *)playerItem { + return [AVPlayer playerWithPlayerItem:playerItem]; +} + +@end + @interface FLTVideoPlayer : NSObject @property(readonly, nonatomic) AVPlayer *player; @property(readonly, nonatomic) AVPlayerItemVideoOutput *videoOutput; @@ -52,7 +63,8 @@ @interface FLTVideoPlayer : NSObject @property(nonatomic, readonly) BOOL isInitialized; - (instancetype)initWithURL:(NSURL *)url frameUpdater:(FLTFrameUpdater *)frameUpdater - httpHeaders:(nonnull NSDictionary *)headers; + httpHeaders:(nonnull NSDictionary *)headers + playerFactory:(id)playerFactory; @end static void *timeRangeContext = &timeRangeContext; @@ -65,9 +77,14 @@ - (instancetype)initWithURL:(NSURL *)url static void *rateContext = &rateContext; @implementation FLTVideoPlayer -- (instancetype)initWithAsset:(NSString *)asset frameUpdater:(FLTFrameUpdater *)frameUpdater { +- (instancetype)initWithAsset:(NSString *)asset + frameUpdater:(FLTFrameUpdater *)frameUpdater + playerFactory:(id)playerFactory { NSString *path = [[NSBundle mainBundle] pathForResource:asset ofType:nil]; - return [self initWithURL:[NSURL fileURLWithPath:path] frameUpdater:frameUpdater httpHeaders:@{}]; + return [self initWithURL:[NSURL fileURLWithPath:path] + frameUpdater:frameUpdater + httpHeaders:@{} + playerFactory:playerFactory]; } - (void)addObserversForItem:(AVPlayerItem *)item player:(AVPlayer *)player { @@ -203,18 +220,20 @@ - (void)createVideoOutputAndDisplayLink:(FLTFrameUpdater *)frameUpdater { - (instancetype)initWithURL:(NSURL *)url frameUpdater:(FLTFrameUpdater *)frameUpdater - httpHeaders:(nonnull NSDictionary *)headers { + httpHeaders:(nonnull NSDictionary *)headers + playerFactory:(id)playerFactory { NSDictionary *options = nil; if ([headers count] != 0) { options = @{@"AVURLAssetHTTPHeaderFieldsKey" : headers}; } AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:options]; AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset]; - return [self initWithPlayerItem:item frameUpdater:frameUpdater]; + return [self initWithPlayerItem:item frameUpdater:frameUpdater playerFactory:playerFactory]; } - (instancetype)initWithPlayerItem:(AVPlayerItem *)item - frameUpdater:(FLTFrameUpdater *)frameUpdater { + frameUpdater:(FLTFrameUpdater *)frameUpdater + playerFactory:(id)playerFactory { self = [super init]; NSAssert(self, @"super init cannot be nil"); @@ -247,7 +266,7 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item } }; - _player = [AVPlayer playerWithPlayerItem:item]; + _player = [playerFactory playerWithPlayerItem:item]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 @@ -420,9 +439,15 @@ - (int64_t)duration { } - (void)seekTo:(int)location completionHandler:(void (^)(BOOL))completionHandler { - [_player seekToTime:CMTimeMake(location, 1000) - toleranceBefore:kCMTimeZero - toleranceAfter:kCMTimeZero + CMTime locationCMT = CMTimeMake(location, 1000); + CMTimeValue duration = _player.currentItem.asset.duration.value; + // Without adding tolerance when seeking to duration, + // seekToTime will never complete, and this call will hang. + // see issue https://github.com/flutter/flutter/issues/124475. + CMTime tolerance = location == duration ? CMTimeMake(1, 1000) : kCMTimeZero; + [_player seekToTime:locationCMT + toleranceBefore:tolerance + toleranceAfter:tolerance completionHandler:completionHandler]; } @@ -523,6 +548,7 @@ @interface FLTVideoPlayerPlugin () @property(readonly, strong, nonatomic) NSMutableDictionary *playersByTextureId; @property(readonly, strong, nonatomic) NSObject *registrar; +@property(nonatomic, strong) id playerFactory; @end @implementation FLTVideoPlayerPlugin @@ -533,11 +559,17 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } - (instancetype)initWithRegistrar:(NSObject *)registrar { + return [self initWithPlayerFactory:[[FVPDefaultPlayerFactory alloc] init] registrar:registrar]; +} + +- (instancetype)initWithPlayerFactory:(id)playerFactory + registrar:(NSObject *)registrar { self = [super init]; NSAssert(self, @"super init cannot be nil"); _registry = [registrar textures]; _messenger = [registrar messenger]; _registrar = registrar; + _playerFactory = playerFactory; _playersByTextureId = [NSMutableDictionary dictionaryWithCapacity:1]; return self; } @@ -588,12 +620,15 @@ - (FLTTextureMessage *)create:(FLTCreateMessage *)input error:(FlutterError **)e } else { assetPath = [_registrar lookupKeyForAsset:input.asset]; } - player = [[FLTVideoPlayer alloc] initWithAsset:assetPath frameUpdater:frameUpdater]; + player = [[FLTVideoPlayer alloc] initWithAsset:assetPath + frameUpdater:frameUpdater + playerFactory:_playerFactory]; return [self onPlayerSetup:player frameUpdater:frameUpdater]; } else if (input.uri) { player = [[FLTVideoPlayer alloc] initWithURL:[NSURL URLWithString:input.uri] frameUpdater:frameUpdater - httpHeaders:input.httpHeaders]; + httpHeaders:input.httpHeaders + playerFactory:_playerFactory]; return [self onPlayerSetup:player frameUpdater:frameUpdater]; } else { *error = [FlutterError errorWithCode:@"video_player" message:@"not implemented" details:nil]; diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin_Test.h b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin_Test.h new file mode 100644 index 000000000000..4c52ba00f963 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin_Test.h @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTVideoPlayerPlugin.h" + +#import + +// Protocol for an AVPlayer instance factory. Used for injecting players in tests. +@protocol FVPPlayerFactory +- (AVPlayer *)playerWithPlayerItem:(AVPlayerItem *)playerItem; +@end + +@interface FLTVideoPlayerPlugin () + +- (instancetype)initWithPlayerFactory:(id)playerFactory + registrar:(NSObject *)registrar; +@end diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 53bbe63fce3d..093f5ebbb51b 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 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.4.5 +version: 2.4.6 environment: sdk: ">=2.18.0 <4.0.0"