diff --git a/RCTVideo.m b/RCTVideo.m index ec886025..a45fa7d6 100644 --- a/RCTVideo.m +++ b/RCTVideo.m @@ -7,6 +7,8 @@ static NSString *const statusKeyPath = @"status"; static NSString *const playbackLikelyToKeepUpKeyPath = @"playbackLikelyToKeepUp"; static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; +static NSString *const readyForDisplayKeyPath = @"readyForDisplay"; +static NSString *const playbackRate = @"rate"; @implementation RCTVideo { @@ -36,6 +38,7 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; BOOL _muted; BOOL _paused; BOOL _repeat; + BOOL _playbackStalled; NSString * _resizeMode; BOOL _fullscreenPlayerPresented; UIViewController * _presentingViewController; @@ -46,6 +49,7 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; if ((self = [super init])) { _eventDispatcher = eventDispatcher; + _playbackStalled = NO; _rate = 1.0; _volume = 1.0; _resizeMode = @"AVLayerVideoGravityResizeAspectFill"; @@ -213,13 +217,13 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; [self addPlayerItemObservers]; [_player pause]; - [_playerLayer removeFromSuperlayer]; - _playerLayer = nil; + [self removePlayerLayer]; [_playerViewController.view removeFromSuperview]; _playerViewController = nil; _player = [AVPlayer playerWithPlayerItem:_playerItem]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; + [_player addObserver:self forKeyPath:playbackRate options:0 context:nil]; const Float64 progressUpdateIntervalMS = _progressUpdateInterval / 1000; // @see endScrubbing in AVPlayerDemoPlaybackViewController.m of https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Introduction/Intro.html @@ -321,6 +325,25 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; } _playerBufferEmpty = NO; } + } else if (object == _playerLayer) { + if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey]) { + if([change objectForKey:NSKeyValueChangeNewKey]) { + [_eventDispatcher sendInputEventWithName:@"onReadyForDisplay" + body:@{@"target": self.reactTag}]; + } + } + } else if (object == _player) { + if([keyPath isEqualToString:playbackRate]) { + [_eventDispatcher sendInputEventWithName:@"onPlaybackRateChange" + body:@{@"playbackRate": [NSNumber numberWithFloat:_player.rate], + @"target": self.reactTag}]; + if(_playbackStalled && _player.rate > 0) { + [_eventDispatcher sendInputEventWithName:@"onPlaybackResume" + body:@{@"playbackRate": [NSNumber numberWithFloat:_player.rate], + @"target": self.reactTag}]; + _playbackStalled = NO; + } + } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } @@ -333,6 +356,16 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:[_player currentItem]]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(playbackStalled:) + name:AVPlayerItemPlaybackStalledNotification + object:nil]; +} + +- (void)playbackStalled:(NSNotification *)notification +{ + [_eventDispatcher sendInputEventWithName:@"onPlaybackStalled" body:@{@"target": self.reactTag}]; + _playbackStalled = YES; } - (void)playerItemDidReachEnd:(NSNotification *)notification @@ -517,6 +550,8 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; _playerLayer.frame = self.bounds; _playerLayer.needsDisplayOnBoundsChange = YES; + + [_playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil]; [self.layer addSublayer:_playerLayer]; self.layer.needsDisplayOnBoundsChange = YES; @@ -530,8 +565,7 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; _controls = controls; if( _controls ) { - [_playerLayer removeFromSuperlayer]; - _playerLayer = nil; + [self removePlayerLayer]; [self usePlayerViewController]; } else @@ -543,6 +577,13 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; } } +- (void)removePlayerLayer +{ + [_playerLayer removeFromSuperlayer]; + [_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath]; + _playerLayer = nil; +} + #pragma mark - RCTVideoPlayerViewControllerDelegate - (void)videoPlayerViewControllerWillDismiss:(AVPlayerViewController *)playerViewController @@ -626,10 +667,10 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; - (void)removeFromSuperview { [_player pause]; + [_player removeObserver:self forKeyPath:playbackRate]; _player = nil; - [_playerLayer removeFromSuperlayer]; - _playerLayer = nil; + [self removePlayerLayer]; [_playerViewController.view removeFromSuperview]; _playerViewController = nil; diff --git a/RCTVideoManager.m b/RCTVideoManager.m index b75e379c..1eb56369 100644 --- a/RCTVideoManager.m +++ b/RCTVideoManager.m @@ -28,7 +28,11 @@ RCT_EXPORT_MODULE(); @"onVideoFullscreenPlayerWillPresent", @"onVideoFullscreenPlayerDidPresent", @"onVideoFullscreenPlayerWillDismiss", - @"onVideoFullscreenPlayerDidDismiss" + @"onVideoFullscreenPlayerDidDismiss", + @"onReadyForDisplay", + @"onPlaybackStalled", + @"onPlaybackResume", + @"onPlaybackRateChange" ]; } diff --git a/Video.js b/Video.js index 38d7b23e..32e2bb07 100644 --- a/Video.js +++ b/Video.js @@ -38,6 +38,10 @@ export default class Video extends Component { this._onFullscreenPlayerDidPresent = this._onFullscreenPlayerDidPresent.bind(this); this._onFullscreenPlayerWillDismiss = this._onFullscreenPlayerWillDismiss.bind(this); this._onFullscreenPlayerDidDismiss = this._onFullscreenPlayerDidDismiss.bind(this); + this._onReadyForDisplay = this._onReadyForDisplay.bind(this); + this._onPlaybackStalled = this._onPlaybackStalled.bind(this); + this._onPlaybackResume = this._onPlaybackResume.bind(this); + this._onPlaybackRateChange = this._onPlaybackRateChange.bind(this); } setNativeProps(nativeProps) { @@ -120,6 +124,30 @@ export default class Video extends Component { } } + _onReadyForDisplay(event) { + if (this.props.onReadyForDisplay) { + this.props.onReadyForDisplay(event.nativeEvent); + } + } + + _onPlaybackStalled(event) { + if (this.props.onPlaybackStalled) { + this.props.onPlaybackStalled(event.nativeEvent); + } + } + + _onPlaybackResume(event) { + if (this.props.onPlaybackResume) { + this.props.onPlaybackResume(event.nativeEvent); + } + } + + _onPlaybackRateChange(event) { + if (this.props.onPlaybackRateChange) { + this.props.onPlaybackRateChange(event.nativeEvent); + } + } + render() { const { source, @@ -165,6 +193,10 @@ export default class Video extends Component { onVideoFullscreenPlayerDidPresent: this._onFullscreenPlayerDidPresent, onVideoFullscreenPlayerWillDismiss: this._onFullscreenPlayerWillDismiss, onVideoFullscreenPlayerDidDismiss: this._onFullscreenPlayerDidDismiss, + onReadyForDisplay: this._onReadyForDisplay, + onPlaybackStalled: this._onPlaybackStalled, + onPlaybackResume: this._onPlaybackResume, + onPlaybackRateChange: this._onPlaybackRateChange, }); return ( @@ -202,6 +234,10 @@ Video.propTypes = { onFullscreenPlayerDidPresent: PropTypes.func, onFullscreenPlayerWillDismiss: PropTypes.func, onFullscreenPlayerDidDismiss: PropTypes.func, + onReadyForDisplay: PropTypes.func, + onPlaybackStalled: PropTypes.func, + onPlaybackResume: PropTypes.func, + onPlaybackRateChange: PropTypes.func, /* Required by react-native */ scaleX: React.PropTypes.number,