-
Notifications
You must be signed in to change notification settings - Fork 3.4k
[video_player_avfoundation] Make sure the AVPlayerItem
is .readyToPlay
before emitting an initialized
event
#9534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
42a5ba4
375ec1f
bb89c41
1ad1d2e
63d240b
0cfe3f4
60fc5d2
f3fbff5
bcc2725
733f5ea
c86f933
35c9a08
3b0cd77
4315c4a
47ff5d0
e2ce9fd
6f232f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,8 +11,6 @@ | |
|
||
static void *timeRangeContext = &timeRangeContext; | ||
static void *statusContext = &statusContext; | ||
static void *presentationSizeContext = &presentationSizeContext; | ||
static void *durationContext = &durationContext; | ||
static void *playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext; | ||
static void *rateContext = &rateContext; | ||
|
||
|
@@ -144,14 +142,6 @@ - (void)addObserversForItem:(AVPlayerItem *)item player:(AVPlayer *)player { | |
forKeyPath:@"status" | ||
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew | ||
context:statusContext]; | ||
[item addObserver:self | ||
forKeyPath:@"presentationSize" | ||
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew | ||
context:presentationSizeContext]; | ||
[item addObserver:self | ||
forKeyPath:@"duration" | ||
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew | ||
context:durationContext]; | ||
[item addObserver:self | ||
forKeyPath:@"playbackLikelyToKeepUp" | ||
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew | ||
|
@@ -176,6 +166,7 @@ - (void)itemDidPlayToEndTime:(NSNotification *)notification { | |
AVPlayerItem *p = [notification object]; | ||
[p seekToTime:kCMTimeZero completionHandler:nil]; | ||
} else { | ||
NSAssert([NSThread isMainThread], @"event sink must only be accessed from the main thread"); | ||
if (_eventSink) { | ||
_eventSink(@{@"event" : @"completed"}); | ||
} | ||
|
@@ -245,56 +236,53 @@ - (void)observeValueForKeyPath:(NSString *)path | |
ofObject:(id)object | ||
change:(NSDictionary *)change | ||
context:(void *)context { | ||
NSAssert([NSThread isMainThread], @"event sink must only be accessed from the main thread"); | ||
if (context == timeRangeContext) { | ||
if (_eventSink != nil) { | ||
NSMutableArray<NSArray<NSNumber *> *> *values = [[NSMutableArray alloc] init]; | ||
for (NSValue *rangeValue in [object loadedTimeRanges]) { | ||
CMTimeRange range = [rangeValue CMTimeRangeValue]; | ||
int64_t start = FVPCMTimeToMillis(range.start); | ||
[values addObject:@[ @(start), @(start + FVPCMTimeToMillis(range.duration)) ]]; | ||
} | ||
_eventSink(@{@"event" : @"bufferingUpdate", @"values" : values}); | ||
if (!_eventSink) { | ||
return; | ||
} | ||
NSMutableArray<NSArray<NSNumber *> *> *values = [[NSMutableArray alloc] init]; | ||
for (NSValue *rangeValue in [object loadedTimeRanges]) { | ||
CMTimeRange range = [rangeValue CMTimeRangeValue]; | ||
int64_t start = FVPCMTimeToMillis(range.start); | ||
[values addObject:@[ @(start), @(start + FVPCMTimeToMillis(range.duration)) ]]; | ||
} | ||
_eventSink(@{@"event" : @"bufferingUpdate", @"values" : values}); | ||
} else if (context == statusContext) { | ||
AVPlayerItem *item = (AVPlayerItem *)object; | ||
switch (item.status) { | ||
case AVPlayerItemStatusFailed: | ||
[self sendFailedToLoadVideoEvent]; | ||
break; | ||
case AVPlayerItemStatusUnknown: | ||
break; | ||
case AVPlayerItemStatusReadyToPlay: | ||
[item addOutput:_videoOutput]; | ||
[self setupEventSinkIfReadyToPlay]; | ||
if (![item.outputs containsObject:_videoOutput]) { | ||
[item addOutput:_videoOutput]; | ||
} | ||
if (_eventSink) { | ||
[self sendVideoInitializedEvent]; | ||
} | ||
break; | ||
case AVPlayerItemStatusFailed: | ||
if (_eventSink) { | ||
[self sendFailedToLoadVideoEvent]; | ||
} | ||
break; | ||
} | ||
} else if (context == presentationSizeContext || context == durationContext) { | ||
AVPlayerItem *item = (AVPlayerItem *)object; | ||
if (item.status == AVPlayerItemStatusReadyToPlay) { | ||
// Due to an apparent bug, when the player item is ready, it still may not have determined | ||
// its presentation size or duration. When these properties are finally set, re-check if | ||
// all required properties and instantiate the event sink if it is not already set up. | ||
[self setupEventSinkIfReadyToPlay]; | ||
} | ||
} else if (context == playbackLikelyToKeepUpContext) { | ||
[self updatePlayingState]; | ||
if ([[_player currentItem] isPlaybackLikelyToKeepUp]) { | ||
if (_eventSink != nil) { | ||
_eventSink(@{@"event" : @"bufferingEnd"}); | ||
} | ||
} else { | ||
if (_eventSink != nil) { | ||
_eventSink(@{@"event" : @"bufferingStart"}); | ||
} | ||
if (!_eventSink) { | ||
return; | ||
} | ||
[self updatePlayingState]; | ||
NSString *event = | ||
[[_player currentItem] isPlaybackLikelyToKeepUp] ? @"bufferingEnd" : @"bufferingStart"; | ||
_eventSink(@{@"event" : event}); | ||
} else if (context == rateContext) { | ||
if (!_eventSink) { | ||
return; | ||
} | ||
// Important: Make sure to cast the object to AVPlayer when observing the rate property, | ||
// as it is not available in AVPlayerItem. | ||
AVPlayer *player = (AVPlayer *)object; | ||
if (_eventSink != nil) { | ||
_eventSink( | ||
@{@"event" : @"isPlayingStateUpdate", @"isPlaying" : player.rate > 0 ? @YES : @NO}); | ||
} | ||
_eventSink(@{@"event" : @"isPlayingStateUpdate", @"isPlaying" : player.rate > 0 ? @YES : @NO}); | ||
} | ||
} | ||
|
||
|
@@ -344,9 +332,7 @@ - (void)updateRate { | |
} | ||
|
||
- (void)sendFailedToLoadVideoEvent { | ||
if (_eventSink == nil) { | ||
return; | ||
} | ||
NSAssert(_eventSink, @"sendFailedToLoadVideoEvent was called when the event sink was nil."); | ||
// Prefer more detailed error information from tracks loading. | ||
NSError *error; | ||
if ([self.player.currentItem.asset statusOfValueForKey:@"tracks" | ||
|
@@ -369,58 +355,24 @@ - (void)sendFailedToLoadVideoEvent { | |
_eventSink([FlutterError errorWithCode:@"VideoError" message:message details:nil]); | ||
} | ||
|
||
- (void)setupEventSinkIfReadyToPlay { | ||
if (_eventSink && !_isInitialized) { | ||
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; | ||
// Audio-only HLS files have no size, so `currentItem.tracks.count` must be used to check for | ||
// track presence, as AVAsset does not always provide track information in HLS streams. | ||
BOOL hasNoTracks = currentItem.tracks.count == 0 && asset.tracks.count == 0; | ||
|
||
// The player has not yet initialized when it has no size, unless it is an audio-only track. | ||
// HLS m3u8 video files never load any tracks, and are also not yet initialized until they have | ||
// a size. | ||
if ((hasVideoTracks || hasNoTracks) && height == CGSizeZero.height && | ||
width == CGSizeZero.width) { | ||
return; | ||
} | ||
// The player may be initialized but still needs to determine the duration. | ||
int64_t duration = [self duration]; | ||
if (duration == 0) { | ||
return; | ||
} | ||
|
||
_isInitialized = YES; | ||
[self updatePlayingState]; | ||
|
||
_eventSink(@{ | ||
@"event" : @"initialized", | ||
@"duration" : @(duration), | ||
@"width" : @(width), | ||
@"height" : @(height) | ||
}); | ||
} | ||
- (void)sendVideoInitializedEvent { | ||
AVPlayerItem *currentItem = self.player.currentItem; | ||
NSAssert(currentItem.status == AVPlayerItemStatusReadyToPlay, | ||
@"sendVideoInitializedEvent was called when the item wasn't ready to play."); | ||
NSAssert(_eventSink, @"sendVideoInitializedEvent was called when the event sink was nil."); | ||
NSAssert(!_isInitialized, @"sendVideoInitializedEvent should only be called once."); | ||
CGSize size = currentItem.presentationSize; | ||
CGFloat width = size.width; | ||
CGFloat height = size.height; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the duration / size checks since now the caller has to guarantee the item is Before this you may enter this |
||
_isInitialized = YES; | ||
[self updatePlayingState]; | ||
_eventSink(@{ | ||
@"event" : @"initialized", | ||
@"duration" : @(self.duration), | ||
@"width" : @(width), | ||
@"height" : @(height) | ||
}); | ||
} | ||
|
||
#pragma mark - FVPVideoPlayerInstanceApi | ||
|
@@ -474,27 +426,32 @@ - (void)setPlaybackSpeed:(double)speed error:(FlutterError *_Nullable *_Nonnull) | |
#pragma mark - FlutterStreamHandler | ||
|
||
- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { | ||
NSAssert([NSThread isMainThread], @"event sink must only be accessed from the main thread"); | ||
_eventSink = nil; | ||
_isInitialized = NO; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So this makes sure the new event sink gets an |
||
return nil; | ||
} | ||
|
||
- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments | ||
eventSink:(nonnull FlutterEventSink)events { | ||
NSAssert([NSThread isMainThread], @"event sink must only be accessed from the main thread"); | ||
_eventSink = events; | ||
// TODO(@recastrodiaz): remove the line below when the race condition is resolved: | ||
// https://github.com/flutter/flutter/issues/21483 | ||
// This line ensures the 'initialized' event is sent when the event | ||
// 'AVPlayerItemStatusReadyToPlay' fires before _eventSink is set (this function | ||
// onListenWithArguments is called) | ||
// and also send error in similar case with 'AVPlayerItemStatusFailed' | ||
// https://github.com/flutter/flutter/issues/151475 | ||
// https://github.com/flutter/flutter/issues/147707 | ||
if (self.player.currentItem.status == AVPlayerItemStatusFailed) { | ||
[self sendFailedToLoadVideoEvent]; | ||
return nil; | ||
switch (self.player.currentItem.status) { | ||
case AVPlayerItemStatusUnknown: | ||
// When this method is called when the media is still loading, do nothing: | ||
// sendVideoInitializedEvent or sendFailedToLoadVideoEvent will be called | ||
// by KVO on status updates. | ||
return nil; | ||
case AVPlayerItemStatusReadyToPlay: | ||
[self sendVideoInitializedEvent]; | ||
return nil; | ||
case AVPlayerItemStatusFailed: | ||
[self sendFailedToLoadVideoEvent]; | ||
return nil; | ||
default: | ||
NSAssert(NO, @"Unknown AVPlayerItemStatus: %ld", (long)self.player.currentItem.status); | ||
return nil; | ||
} | ||
[self setupEventSinkIfReadyToPlay]; | ||
return nil; | ||
} | ||
|
||
#pragma mark - Private | ||
|
@@ -513,8 +470,6 @@ - (void)removeKeyValueObservers { | |
AVPlayerItem *currentItem = _player.currentItem; | ||
[currentItem removeObserver:self forKeyPath:@"status"]; | ||
[currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"]; | ||
[currentItem removeObserver:self forKeyPath:@"presentationSize"]; | ||
[currentItem removeObserver:self forKeyPath:@"duration"]; | ||
[currentItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"]; | ||
[_player removeObserver:self forKeyPath:@"rate"]; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What workarounds?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The size / duration checks removed in the
sendInitialized
method.