Skip to content

Commit 2323aa4

Browse files
authored
[video_player] foundation - reduce seek accuracy to fix seek to end bug (flutter#3784)
Reduces avplayer seek accuracy when seeking to end of video to fix `seekToTime` never running completion handler. fixes flutter#124475
1 parent 3300ec5 commit 2323aa4

File tree

6 files changed

+205
-18
lines changed

6 files changed

+205
-18
lines changed

packages/video_player/video_player_avfoundation/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.4.6
2+
3+
* Fixes hang when seeking to end of video.
4+
15
## 2.4.5
26

37
* Updates functions without a prototype to avoid deprecation warning.

packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,27 @@ void main() {
7171
expect(await controller.position, greaterThan(Duration.zero));
7272
});
7373

74-
testWidgets('can seek', (WidgetTester tester) async {
75-
await controller.initialize();
74+
testWidgets(
75+
'can seek',
76+
(WidgetTester tester) async {
77+
await controller.initialize();
7678

77-
await controller.seekTo(const Duration(seconds: 3));
79+
await controller.seekTo(const Duration(seconds: 3));
7880

79-
expect(await controller.position, const Duration(seconds: 3));
80-
});
81+
expect(controller.value.position, const Duration(seconds: 3));
82+
},
83+
);
84+
85+
testWidgets(
86+
'can seek to end',
87+
(WidgetTester tester) async {
88+
await controller.initialize();
89+
90+
await controller.seekTo(controller.value.duration);
91+
92+
expect(controller.value.duration, controller.value.position);
93+
},
94+
);
8195

8296
testWidgets('can be paused', (WidgetTester tester) async {
8397
await controller.initialize();

packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#import <OCMock/OCMock.h>
1010
#import <video_player_avfoundation/AVAssetTrackUtils.h>
11+
#import <video_player_avfoundation/FLTVideoPlayerPlugin_Test.h>
1112

1213
@interface FLTVideoPlayer : NSObject <FlutterStreamHandler>
1314
@property(readonly, nonatomic) AVPlayer *player;
@@ -61,6 +62,46 @@ - (CGAffineTransform)preferredTransform {
6162
@interface VideoPlayerTests : XCTestCase
6263
@end
6364

65+
@interface StubAVPlayer : AVPlayer
66+
@property(readonly, nonatomic) NSNumber *beforeTolerance;
67+
@property(readonly, nonatomic) NSNumber *afterTolerance;
68+
@end
69+
70+
@implementation StubAVPlayer
71+
72+
- (void)seekToTime:(CMTime)time
73+
toleranceBefore:(CMTime)toleranceBefore
74+
toleranceAfter:(CMTime)toleranceAfter
75+
completionHandler:(void (^)(BOOL finished))completionHandler {
76+
_beforeTolerance = [NSNumber numberWithLong:toleranceBefore.value];
77+
_afterTolerance = [NSNumber numberWithLong:toleranceAfter.value];
78+
completionHandler(YES);
79+
}
80+
81+
@end
82+
83+
@interface StubFVPPlayerFactory : NSObject <FVPPlayerFactory>
84+
85+
@property(nonatomic, strong) StubAVPlayer *stubAVPlayer;
86+
87+
- (instancetype)initWithPlayer:(StubAVPlayer *)stubAVPlayer;
88+
89+
@end
90+
91+
@implementation StubFVPPlayerFactory
92+
93+
- (instancetype)initWithPlayer:(StubAVPlayer *)stubAVPlayer {
94+
self = [super init];
95+
_stubAVPlayer = stubAVPlayer;
96+
return self;
97+
}
98+
99+
- (AVPlayer *)playerWithPlayerItem:(AVPlayerItem *)playerItem {
100+
return _stubAVPlayer;
101+
}
102+
103+
@end
104+
64105
@implementation VideoPlayerTests
65106

66107
- (void)testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream {
@@ -226,6 +267,81 @@ - (void)testTransformFix {
226267
[self validateTransformFixForOrientation:UIImageOrientationRightMirrored];
227268
}
228269

270+
- (void)testSeekToleranceWhenNotSeekingToEnd {
271+
NSObject<FlutterPluginRegistry> *registry =
272+
(NSObject<FlutterPluginRegistry> *)[[UIApplication sharedApplication] delegate];
273+
NSObject<FlutterPluginRegistrar> *registrar = [registry registrarForPlugin:@"TestSeekTolerance"];
274+
275+
StubAVPlayer *stubAVPlayer = [[StubAVPlayer alloc] init];
276+
StubFVPPlayerFactory *stubFVPPlayerFactory =
277+
[[StubFVPPlayerFactory alloc] initWithPlayer:stubAVPlayer];
278+
FLTVideoPlayerPlugin *pluginWithMockAVPlayer =
279+
[[FLTVideoPlayerPlugin alloc] initWithPlayerFactory:stubFVPPlayerFactory registrar:registrar];
280+
281+
FlutterError *error;
282+
[pluginWithMockAVPlayer initialize:&error];
283+
XCTAssertNil(error);
284+
285+
FLTCreateMessage *create = [FLTCreateMessage
286+
makeWithAsset:nil
287+
uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
288+
packageName:nil
289+
formatHint:nil
290+
httpHeaders:@{}];
291+
FLTTextureMessage *textureMessage = [pluginWithMockAVPlayer create:create error:&error];
292+
NSNumber *textureId = textureMessage.textureId;
293+
294+
XCTestExpectation *initializedExpectation =
295+
[self expectationWithDescription:@"seekTo has zero tolerance when seeking not to end"];
296+
FLTPositionMessage *message = [FLTPositionMessage makeWithTextureId:textureId position:@1234];
297+
[pluginWithMockAVPlayer seekTo:message
298+
completion:^(FlutterError *_Nullable error) {
299+
[initializedExpectation fulfill];
300+
}];
301+
302+
[self waitForExpectationsWithTimeout:30.0 handler:nil];
303+
XCTAssertEqual([stubAVPlayer.beforeTolerance intValue], 0);
304+
XCTAssertEqual([stubAVPlayer.afterTolerance intValue], 0);
305+
}
306+
307+
- (void)testSeekToleranceWhenSeekingToEnd {
308+
NSObject<FlutterPluginRegistry> *registry =
309+
(NSObject<FlutterPluginRegistry> *)[[UIApplication sharedApplication] delegate];
310+
NSObject<FlutterPluginRegistrar> *registrar =
311+
[registry registrarForPlugin:@"TestSeekToEndTolerance"];
312+
313+
StubAVPlayer *stubAVPlayer = [[StubAVPlayer alloc] init];
314+
StubFVPPlayerFactory *stubFVPPlayerFactory =
315+
[[StubFVPPlayerFactory alloc] initWithPlayer:stubAVPlayer];
316+
FLTVideoPlayerPlugin *pluginWithMockAVPlayer =
317+
[[FLTVideoPlayerPlugin alloc] initWithPlayerFactory:stubFVPPlayerFactory registrar:registrar];
318+
319+
FlutterError *error;
320+
[pluginWithMockAVPlayer initialize:&error];
321+
XCTAssertNil(error);
322+
323+
FLTCreateMessage *create = [FLTCreateMessage
324+
makeWithAsset:nil
325+
uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
326+
packageName:nil
327+
formatHint:nil
328+
httpHeaders:@{}];
329+
FLTTextureMessage *textureMessage = [pluginWithMockAVPlayer create:create error:&error];
330+
NSNumber *textureId = textureMessage.textureId;
331+
332+
XCTestExpectation *initializedExpectation =
333+
[self expectationWithDescription:@"seekTo has non-zero tolerance when seeking to end"];
334+
// The duration of this video is "0" due to the non standard initiliatazion process.
335+
FLTPositionMessage *message = [FLTPositionMessage makeWithTextureId:textureId position:@0];
336+
[pluginWithMockAVPlayer seekTo:message
337+
completion:^(FlutterError *_Nullable error) {
338+
[initializedExpectation fulfill];
339+
}];
340+
[self waitForExpectationsWithTimeout:30.0 handler:nil];
341+
XCTAssertGreaterThan([stubAVPlayer.beforeTolerance intValue], 0);
342+
XCTAssertGreaterThan([stubAVPlayer.afterTolerance intValue], 0);
343+
}
344+
229345
- (NSDictionary<NSString *, id> *)testPlugin:(FLTVideoPlayerPlugin *)videoPlayerPlugin
230346
uri:(NSString *)uri {
231347
FlutterError *error;

packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
#import "FLTVideoPlayerPlugin.h"
6+
#import "FLTVideoPlayerPlugin_Test.h"
67

78
#import <AVFoundation/AVFoundation.h>
89
#import <GLKit/GLKit.h>
@@ -33,6 +34,16 @@ - (void)onDisplayLink:(CADisplayLink *)link {
3334
}
3435
@end
3536

37+
@interface FVPDefaultPlayerFactory : NSObject <FVPPlayerFactory>
38+
@end
39+
40+
@implementation FVPDefaultPlayerFactory
41+
- (AVPlayer *)playerWithPlayerItem:(AVPlayerItem *)playerItem {
42+
return [AVPlayer playerWithPlayerItem:playerItem];
43+
}
44+
45+
@end
46+
3647
@interface FLTVideoPlayer : NSObject <FlutterTexture, FlutterStreamHandler>
3748
@property(readonly, nonatomic) AVPlayer *player;
3849
@property(readonly, nonatomic) AVPlayerItemVideoOutput *videoOutput;
@@ -52,7 +63,8 @@ @interface FLTVideoPlayer : NSObject <FlutterTexture, FlutterStreamHandler>
5263
@property(nonatomic, readonly) BOOL isInitialized;
5364
- (instancetype)initWithURL:(NSURL *)url
5465
frameUpdater:(FLTFrameUpdater *)frameUpdater
55-
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers;
66+
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers
67+
playerFactory:(id<FVPPlayerFactory>)playerFactory;
5668
@end
5769

5870
static void *timeRangeContext = &timeRangeContext;
@@ -65,9 +77,14 @@ - (instancetype)initWithURL:(NSURL *)url
6577
static void *rateContext = &rateContext;
6678

6779
@implementation FLTVideoPlayer
68-
- (instancetype)initWithAsset:(NSString *)asset frameUpdater:(FLTFrameUpdater *)frameUpdater {
80+
- (instancetype)initWithAsset:(NSString *)asset
81+
frameUpdater:(FLTFrameUpdater *)frameUpdater
82+
playerFactory:(id<FVPPlayerFactory>)playerFactory {
6983
NSString *path = [[NSBundle mainBundle] pathForResource:asset ofType:nil];
70-
return [self initWithURL:[NSURL fileURLWithPath:path] frameUpdater:frameUpdater httpHeaders:@{}];
84+
return [self initWithURL:[NSURL fileURLWithPath:path]
85+
frameUpdater:frameUpdater
86+
httpHeaders:@{}
87+
playerFactory:playerFactory];
7188
}
7289

7390
- (void)addObserversForItem:(AVPlayerItem *)item player:(AVPlayer *)player {
@@ -203,18 +220,20 @@ - (void)createVideoOutputAndDisplayLink:(FLTFrameUpdater *)frameUpdater {
203220

204221
- (instancetype)initWithURL:(NSURL *)url
205222
frameUpdater:(FLTFrameUpdater *)frameUpdater
206-
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers {
223+
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers
224+
playerFactory:(id<FVPPlayerFactory>)playerFactory {
207225
NSDictionary<NSString *, id> *options = nil;
208226
if ([headers count] != 0) {
209227
options = @{@"AVURLAssetHTTPHeaderFieldsKey" : headers};
210228
}
211229
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:options];
212230
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];
213-
return [self initWithPlayerItem:item frameUpdater:frameUpdater];
231+
return [self initWithPlayerItem:item frameUpdater:frameUpdater playerFactory:playerFactory];
214232
}
215233

216234
- (instancetype)initWithPlayerItem:(AVPlayerItem *)item
217-
frameUpdater:(FLTFrameUpdater *)frameUpdater {
235+
frameUpdater:(FLTFrameUpdater *)frameUpdater
236+
playerFactory:(id<FVPPlayerFactory>)playerFactory {
218237
self = [super init];
219238
NSAssert(self, @"super init cannot be nil");
220239

@@ -247,7 +266,7 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item
247266
}
248267
};
249268

250-
_player = [AVPlayer playerWithPlayerItem:item];
269+
_player = [playerFactory playerWithPlayerItem:item];
251270
_player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
252271

253272
// This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16
@@ -420,9 +439,15 @@ - (int64_t)duration {
420439
}
421440

422441
- (void)seekTo:(int)location completionHandler:(void (^)(BOOL))completionHandler {
423-
[_player seekToTime:CMTimeMake(location, 1000)
424-
toleranceBefore:kCMTimeZero
425-
toleranceAfter:kCMTimeZero
442+
CMTime locationCMT = CMTimeMake(location, 1000);
443+
CMTimeValue duration = _player.currentItem.asset.duration.value;
444+
// Without adding tolerance when seeking to duration,
445+
// seekToTime will never complete, and this call will hang.
446+
// see issue https://github.com/flutter/flutter/issues/124475.
447+
CMTime tolerance = location == duration ? CMTimeMake(1, 1000) : kCMTimeZero;
448+
[_player seekToTime:locationCMT
449+
toleranceBefore:tolerance
450+
toleranceAfter:tolerance
426451
completionHandler:completionHandler];
427452
}
428453

@@ -523,6 +548,7 @@ @interface FLTVideoPlayerPlugin () <FLTAVFoundationVideoPlayerApi>
523548
@property(readonly, strong, nonatomic)
524549
NSMutableDictionary<NSNumber *, FLTVideoPlayer *> *playersByTextureId;
525550
@property(readonly, strong, nonatomic) NSObject<FlutterPluginRegistrar> *registrar;
551+
@property(nonatomic, strong) id<FVPPlayerFactory> playerFactory;
526552
@end
527553

528554
@implementation FLTVideoPlayerPlugin
@@ -533,11 +559,17 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
533559
}
534560

535561
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
562+
return [self initWithPlayerFactory:[[FVPDefaultPlayerFactory alloc] init] registrar:registrar];
563+
}
564+
565+
- (instancetype)initWithPlayerFactory:(id<FVPPlayerFactory>)playerFactory
566+
registrar:(NSObject<FlutterPluginRegistrar> *)registrar {
536567
self = [super init];
537568
NSAssert(self, @"super init cannot be nil");
538569
_registry = [registrar textures];
539570
_messenger = [registrar messenger];
540571
_registrar = registrar;
572+
_playerFactory = playerFactory;
541573
_playersByTextureId = [NSMutableDictionary dictionaryWithCapacity:1];
542574
return self;
543575
}
@@ -588,12 +620,15 @@ - (FLTTextureMessage *)create:(FLTCreateMessage *)input error:(FlutterError **)e
588620
} else {
589621
assetPath = [_registrar lookupKeyForAsset:input.asset];
590622
}
591-
player = [[FLTVideoPlayer alloc] initWithAsset:assetPath frameUpdater:frameUpdater];
623+
player = [[FLTVideoPlayer alloc] initWithAsset:assetPath
624+
frameUpdater:frameUpdater
625+
playerFactory:_playerFactory];
592626
return [self onPlayerSetup:player frameUpdater:frameUpdater];
593627
} else if (input.uri) {
594628
player = [[FLTVideoPlayer alloc] initWithURL:[NSURL URLWithString:input.uri]
595629
frameUpdater:frameUpdater
596-
httpHeaders:input.httpHeaders];
630+
httpHeaders:input.httpHeaders
631+
playerFactory:_playerFactory];
597632
return [self onPlayerSetup:player frameUpdater:frameUpdater];
598633
} else {
599634
*error = [FlutterError errorWithCode:@"video_player" message:@"not implemented" details:nil];
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#import "FLTVideoPlayerPlugin.h"
6+
7+
#import <AVFoundation/AVFoundation.h>
8+
9+
// Protocol for an AVPlayer instance factory. Used for injecting players in tests.
10+
@protocol FVPPlayerFactory
11+
- (AVPlayer *)playerWithPlayerItem:(AVPlayerItem *)playerItem;
12+
@end
13+
14+
@interface FLTVideoPlayerPlugin ()
15+
16+
- (instancetype)initWithPlayerFactory:(id<FVPPlayerFactory>)playerFactory
17+
registrar:(NSObject<FlutterPluginRegistrar> *)registrar;
18+
@end

packages/video_player/video_player_avfoundation/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: video_player_avfoundation
22
description: iOS implementation of the video_player plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
5-
version: 2.4.5
5+
version: 2.4.6
66

77
environment:
88
sdk: ">=2.18.0 <4.0.0"

0 commit comments

Comments
 (0)