diff --git a/AsyncDisplayKit/ASVideoNode.h b/AsyncDisplayKit/ASVideoNode.h index 224702ecfc..d039517dc4 100644 --- a/AsyncDisplayKit/ASVideoNode.h +++ b/AsyncDisplayKit/ASVideoNode.h @@ -14,9 +14,9 @@ typedef enum { ASVideoNodePlayerStateUnknown, - ASVideoNodePlayerStatePlaying, - ASVideoNodePlayerStateStartupLoading, + ASVideoNodePlayerStateInitialLoading, ASVideoNodePlayerStateLoading, + ASVideoNodePlayerStatePlaying, ASVideoNodePlayerStatePaused, ASVideoNodePlayerStateFinished } ASVideoNodePlayerState; @@ -48,6 +48,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign, readwrite) BOOL shouldAutorepeat; @property (nonatomic, assign, readwrite) BOOL muted; +@property (nonatomic, assign, readwrite) BOOL shouldAggressivelyRecoverFromStall; @property (nonatomic, assign, readonly) ASVideoNodePlayerState playerState; //! Defaults to 100 @@ -69,26 +70,26 @@ NS_ASSUME_NONNULL_BEGIN * @abstract Delegate method invoked when the node's video has played to its end time. * @param videoNode The video node has played to its end time. */ -- (void)videoPlaybackDidFinish:(ASVideoNode *)videoNode; +- (void)videoDidPlayToEnd:(ASVideoNode *)videoNode; /** * @abstract Delegate method invoked the node is tapped. * @param videoNode The video node that was tapped. * @discussion The video's play state is toggled if this method is not implemented. */ -- (void)videoNodeWasTapped:(ASVideoNode *)videoNode; +- (void)didTapVideoNode:(ASVideoNode *)videoNode; /** * @abstract Delegate method invoked when player changes state. - * @param videoNode The video node that was tapped. + * @param videoNode The video node. * @param state player state before this change. - * @param toSate player new state. + * @param toState player new state. * @discussion This method is called after each state change */ -- (void)videoNode:(ASVideoNode *)videoNode willChangePlayerState:(ASVideoNodePlayerState)state toState:(ASVideoNodePlayerState)toSate; +- (void)videoNode:(ASVideoNode *)videoNode willChangePlayerState:(ASVideoNodePlayerState)state toState:(ASVideoNodePlayerState)toState; /** * @abstract Ssks delegate if state change is allowed * ASVideoNodePlayerStatePlaying or ASVideoNodePlayerStatePaused. * asks delegate if state change is allowed. - * @param videoNode The video node that was tapped. + * @param videoNode The video node. * @param state player state that is going to be set. * @discussion Delegate method invoked when player changes it's state to * ASVideoNodePlayerStatePlaying or ASVideoNodePlayerStatePaused @@ -97,10 +98,32 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)videoNode:(ASVideoNode*)videoNode shouldChangePlayerStateTo:(ASVideoNodePlayerState)state; /** * @abstract Delegate method invoked when player playback time is updated. - * @param videoNode The video node that was tapped. + * @param videoNode The video node. * @param second current playback time in seconds. */ -- (void)videoNode:(ASVideoNode *)videoNode didPlayToSecond:(NSTimeInterval)second; +- (void)videoNode:(ASVideoNode *)videoNode didPlayToTimeInterval:(NSTimeInterval)timeInterval; +/** + * @abstract Delegate method invoked when the video player stalls. + * @param videoNode The video node that has experienced the stall + * @param second Current playback time when the stall happens + */ +- (void)videoNode:(ASVideoNode *)videoNode didStallAtTimeInterval:(NSTimeInterval)timeInterval; +/** + * @abstract Delegate method invoked when the video player starts the inital asset loading + * @param videoNode The videoNode + */ +- (void)videoNodeDidStartInitialLoading:(ASVideoNode *)videoNode; +/** + * @abstract Delegate method invoked when the video is done loading the asset and can start the playback + * @param videoNode The videoNode + */ +- (void)videoNodeDidFinishInitialLoading:(ASVideoNode *)videoNode; +/** + * @abstract Delegate method invoked when the video node has recovered from the stall + * @param videoNode The videoNode + */ +- (void)videoNodeDidRecoverFromStall:(ASVideoNode *)videoNode; + @end NS_ASSUME_NONNULL_END -#endif +#endif \ No newline at end of file diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 6a2af7b164..9a837d269a 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -38,17 +38,21 @@ static NSString * const kStatus = @"status"; __weak id _delegate; struct { unsigned int delegateVideNodeShouldChangePlayerStateTo:1; - unsigned int delegateVideoPlaybackDidFinish:1; - unsigned int delegateVideoNodeWasTapped:1; + unsigned int delegateVideoDidPlayToEnd:1; + unsigned int delegateDidTapVideoNode:1; unsigned int delegateVideoNodeWillChangePlayerStateToState:1; - unsigned int delegateVideoNodeDidPlayToSecond:1; + unsigned int delegateVideoNodeDidPlayToTimeInterval:1; + unsigned int delegateVideoNodeDidStartInitialLoading:1; + unsigned int delegateVideoNodeDidFinishInitialLoading:1; + unsigned int delegateVideoNodeDidStallAtTimeInterval:1; + unsigned int delegateVideoNodeDidRecoverFromStall:1; } _delegateFlags; BOOL _shouldBePlaying; BOOL _shouldAutorepeat; BOOL _shouldAutoplay; - + BOOL _shouldAggressivelyRecoverFromStall; BOOL _muted; ASVideoNodePlayerState _playerState; @@ -164,6 +168,7 @@ static NSString * const kStatus = @"status"; NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(didPlayToEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem]; + [notificationCenter addObserver:self selector:@selector(videoNodeDidStall:) name:AVPlayerItemPlaybackStalledNotification object:playerItem]; [notificationCenter addObserver:self selector:@selector(errorWhilePlaying:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem]; [notificationCenter addObserver:self selector:@selector(errorWhilePlaying:) name:AVPlayerItemNewErrorLogEntryNotification object:playerItem]; } @@ -181,6 +186,7 @@ static NSString * const kStatus = @"status"; NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem]; + [notificationCenter removeObserver:self name: AVPlayerItemPlaybackStalledNotification object:playerItem]; [notificationCenter removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem]; [notificationCenter removeObserver:self name:AVPlayerItemNewErrorLogEntryNotification object:playerItem]; } @@ -313,12 +319,12 @@ static NSString * const kStatus = @"status"; if (_placeholderImageNode.image == nil) { [self generatePlaceholderImage]; } - if (_shouldBePlaying) { - self.playerState = ASVideoNodePlayerStatePlaying; - } } } else if ([keyPath isEqualToString:kPlaybackLikelyToKeepUpKey]) { - if (_shouldBePlaying && [change[NSKeyValueChangeNewKey] boolValue] == true && ASInterfaceStateIncludesVisible(self.interfaceState)) { + if (_shouldBePlaying && (_shouldAggressivelyRecoverFromStall || [change[NSKeyValueChangeNewKey] boolValue]) && ASInterfaceStateIncludesVisible(self.interfaceState)) { + if (self.playerState == ASVideoNodePlayerStateLoading && _delegateFlags.delegateVideoNodeDidRecoverFromStall) { + [_delegate videoNodeDidRecoverFromStall:self]; + } [self play]; // autoresume after buffer catches up } } else if ([keyPath isEqualToString:kplaybackBufferEmpty]) { @@ -331,8 +337,8 @@ static NSString * const kStatus = @"status"; - (void)tapped { - if (_delegateFlags.delegateVideoNodeWasTapped) { - [_delegate videoNodeWasTapped:self]; + if (_delegateFlags.delegateDidTapVideoNode) { + [_delegate didTapVideoNode:self]; } else { if (_shouldBePlaying) { [self pause]; @@ -349,10 +355,20 @@ static NSString * const kStatus = @"status"; { ASDN::MutexLocker l(_videoLock); AVAsset *asset = self.asset; + // Return immediately if the asset is nil; + if (asset == nil || self.playerState == ASVideoNodePlayerStateInitialLoading) { + return; + } NSArray *requestedKeys = @[@"playable"]; - self.playerState = ASVideoNodePlayerStateStartupLoading; + self.playerState = ASVideoNodePlayerStateInitialLoading; + if (_delegateFlags.delegateVideoNodeDidStartInitialLoading) { + [_delegate videoNodeDidStartInitialLoading:self]; + } [asset loadValuesAsynchronouslyForKeys:requestedKeys completionHandler:^{ ASPerformBlockOnMainThread(^{ + if (_delegateFlags.delegateVideoNodeDidFinishInitialLoading) { + [_delegate videoNodeDidFinishInitialLoading:self]; + } [self prepareToPlayAsset:asset withKeys:requestedKeys]; }); }]; @@ -366,8 +382,8 @@ static NSString * const kStatus = @"status"; return; } - if (_delegateFlags.delegateVideoNodeDidPlayToSecond) { - [_delegate videoNode:self didPlayToSecond:timeInSeconds]; + if (_delegateFlags.delegateVideoNodeDidPlayToTimeInterval) { + [_delegate videoNode:self didPlayToTimeInterval:timeInSeconds]; } } @@ -482,10 +498,14 @@ static NSString * const kStatus = @"status"; memset(&_delegateFlags, 0, sizeof(_delegateFlags)); } else { _delegateFlags.delegateVideNodeShouldChangePlayerStateTo = [_delegate respondsToSelector:@selector(videoNode:shouldChangePlayerStateTo:)]; - _delegateFlags.delegateVideoPlaybackDidFinish = [_delegate respondsToSelector:@selector(videoPlaybackDidFinish:)]; - _delegateFlags.delegateVideoNodeWasTapped = [_delegate respondsToSelector:@selector(videoNodeWasTapped:)]; + _delegateFlags.delegateVideoDidPlayToEnd = [_delegate respondsToSelector:@selector(videoDidPlayToEnd:)]; + _delegateFlags.delegateDidTapVideoNode = [_delegate respondsToSelector:@selector(didTapVideoNode:)]; _delegateFlags.delegateVideoNodeWillChangePlayerStateToState = [_delegate respondsToSelector:@selector(videoNode:willChangePlayerState:toState:)]; - _delegateFlags.delegateVideoNodeDidPlayToSecond = [_delegate respondsToSelector:@selector(videoNode:didPlayToSecond:)]; + _delegateFlags.delegateVideoNodeDidPlayToTimeInterval = [_delegate respondsToSelector:@selector(videoNode:didPlayToTimeInterval:)]; + _delegateFlags.delegateVideoNodeDidStartInitialLoading = [_delegate respondsToSelector:@selector(videoNodeDidStartInitialLoading:)]; + _delegateFlags.delegateVideoNodeDidFinishInitialLoading = [_delegate respondsToSelector:@selector(videoNodeDidFinishInitialLoading:)]; + _delegateFlags.delegateVideoNodeDidStallAtTimeInterval = [_delegate respondsToSelector:@selector(videoNode:didStallAtTimeInterval:)]; + _delegateFlags.delegateVideoNodeDidRecoverFromStall = [_delegate respondsToSelector:@selector(videoNodeDidRecoverFromStall:)]; } } @@ -632,8 +652,8 @@ static NSString * const kStatus = @"status"; - (void)didPlayToEnd:(NSNotification *)notification { self.playerState = ASVideoNodePlayerStateFinished; - if (_delegateFlags.delegateVideoPlaybackDidFinish) { - [_delegate videoPlaybackDidFinish:self]; + if (_delegateFlags.delegateVideoDidPlayToEnd) { + [_delegate videoDidPlayToEnd:self]; } [_player seekToTime:kCMTimeZero]; @@ -644,6 +664,15 @@ static NSString * const kStatus = @"status"; } } +- (void)videoNodeDidStall:(NSNotification *)notification +{ + self.playerState = ASVideoNodePlayerStateLoading; + [self showSpinner]; + if (_delegateFlags.delegateVideoNodeDidStallAtTimeInterval) { + [_delegate videoNode:self didStallAtTimeInterval:CMTimeGetSeconds(_player.currentItem.currentTime)]; + } +} + - (void)errorWhilePlaying:(NSNotification *)notification { if ([notification.name isEqualToString:AVPlayerItemFailedToPlayToEndTimeNotification]) { @@ -737,4 +766,4 @@ static NSString * const kStatus = @"status"; } @end -#endif +#endif \ No newline at end of file