diff --git a/RCTVideo.h b/RCTVideo.h index 1fc82e43..e0c96663 100644 --- a/RCTVideo.h +++ b/RCTVideo.h @@ -1,5 +1,10 @@ #import "RCTView.h" +extern NSString *const RNVideoLoadedEvent; +extern NSString *const RNVideoLoadingEvent; +extern NSString *const RNVideoProgressEvent; +extern NSString *const RNVideoLoadingErrorEvent; + @class RCTEventDispatcher; @interface RCTVideo : UIView diff --git a/RCTVideo.m b/RCTVideo.m index ceb74afc..8cfa22e3 100644 --- a/RCTVideo.m +++ b/RCTVideo.m @@ -5,9 +5,17 @@ #import "UIView+React.h" #import +NSString *const RNVideoLoadedEvent = @"videoLoaded"; +NSString *const RNVideoLoadingEvent = @"videoLoading"; +NSString *const RNVideoProgressEvent = @"videoProgress"; +NSString *const RNVideoLoadingErrorEvent = @"videoLoadError"; + +static NSString *const statusKeyPath = @"status"; + @implementation RCTVideo { AVPlayer *_player; + AVPlayerItem *_playerItem; AVPlayerLayer *_playerLayer; NSURL *_videoURL; @@ -25,8 +33,7 @@ BOOL _muted; } -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher -{ +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher { if ((self = [super init])) { _eventDispatcher = eventDispatcher; @@ -39,16 +46,15 @@ return self; } -- (void)sendProgressUpdate -{ +- (void)sendProgressUpdate { AVPlayerItem *video = [_player currentItem]; - if (video == nil) { + if (video == nil || video.status != AVPlayerItemStatusReadyToPlay) { return; } - if (_prevProgressUpdateTime == nil || - (([_prevProgressUpdateTime timeIntervalSinceNow] * -1000.0) >= _progressUpdateInterval)) { - [_eventDispatcher sendInputEventWithName:@"videoProgress" body:@{ + if (_prevProgressUpdateTime == nil || + (([_prevProgressUpdateTime timeIntervalSinceNow] * -1000.0) >= _progressUpdateInterval)) { + [_eventDispatcher sendInputEventWithName:RNVideoProgressEvent body:@{ @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(video.currentTime)], @"target": self.reactTag }]; @@ -56,17 +62,25 @@ } } -- (void)setSrc:(NSString *)source -{ - BOOL isHttpPrefix = [source hasPrefix:@"http://"]; - if (isHttpPrefix) { - _videoURL = [NSURL URLWithString:source]; - } - else { - _videoURL = [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:source ofType:@"mp4"]]; - } - _player = [AVPlayer playerWithURL:_videoURL]; +- (void)setSrc:(NSDictionary *)source { + bool isNetwork = [source objectForKey:@"isNetwork"]; + NSString *uri = [source objectForKey:@"uri"]; + NSString *type = [source objectForKey:@"type"]; + + _videoURL = isNetwork ? + [NSURL URLWithString:uri] : + [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]]; + + [_playerItem removeObserver:self forKeyPath:statusKeyPath]; + _playerItem = [AVPlayerItem playerItemWithURL:_videoURL]; + [_playerItem addObserver:self forKeyPath:statusKeyPath options:0 context:nil]; + + [_player pause]; + [_playerLayer removeFromSuperlayer]; + + _player = [AVPlayer playerWithPlayerItem:_playerItem]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; + _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; _playerLayer.frame = self.bounds; _playerLayer.needsDisplayOnBoundsChange = YES; @@ -74,28 +88,48 @@ [self.layer addSublayer:_playerLayer]; self.layer.needsDisplayOnBoundsChange = YES; - AVPlayerItem *video = [_player currentItem]; - - [_eventDispatcher sendInputEventWithName:@"videoLoaded" body:@{ - @"duration": [NSNumber numberWithFloat:CMTimeGetSeconds(video.asset.duration)], - @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(video.currentTime)], - @"canPlayReverse": [NSNumber numberWithBool:video.canPlayReverse], - @"canPlayFastForward": [NSNumber numberWithBool:video.canPlayFastForward], - @"canPlaySlowForward": [NSNumber numberWithBool:video.canPlaySlowForward], - @"canPlaySlowReverse": [NSNumber numberWithBool:video.canPlaySlowReverse], - @"canStepBackward": [NSNumber numberWithBool:video.canStepBackward], - @"canStepForward": [NSNumber numberWithBool:video.canStepForward], + [_eventDispatcher sendInputEventWithName:RNVideoLoadingEvent body:@{ + @"src": @{ + @"uri":uri, + @"type": type, + @"isNetwork":[NSNumber numberWithBool:isNetwork] + }, @"target": self.reactTag }]; - - [_player play]; - - /* rate and volume must be set after play is called */ - [self applyModifiers]; } -- (void)setResizeMode:(NSString*)mode -{ +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (object == _playerItem) { + if (_playerItem.status == AVPlayerItemStatusReadyToPlay) { + [_eventDispatcher sendInputEventWithName:RNVideoLoadedEvent body:@{ + @"duration": [NSNumber numberWithFloat:CMTimeGetSeconds(_playerItem.duration)], + @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(_playerItem.currentTime)], + @"canPlayReverse": [NSNumber numberWithBool:_playerItem.canPlayReverse], + @"canPlayFastForward": [NSNumber numberWithBool:_playerItem.canPlayFastForward], + @"canPlaySlowForward": [NSNumber numberWithBool:_playerItem.canPlaySlowForward], + @"canPlaySlowReverse": [NSNumber numberWithBool:_playerItem.canPlaySlowReverse], + @"canStepBackward": [NSNumber numberWithBool:_playerItem.canStepBackward], + @"canStepForward": [NSNumber numberWithBool:_playerItem.canStepForward], + @"target": self.reactTag + }]; + + [_player play]; + [self applyModifiers]; + } else if(_playerItem.status == AVPlayerItemStatusFailed) { + [_eventDispatcher sendInputEventWithName:RNVideoLoadingErrorEvent body:@{ + @"error": @{ + @"code": [NSNumber numberWithInt:_playerItem.error.code], + @"domain": _playerItem.error.domain + }, + @"target": self.reactTag + }]; + } + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)setResizeMode:(NSString*)mode { _playerLayer.videoGravity = mode; } @@ -147,7 +181,6 @@ [self applyModifiers]; } - - (void)setRepeatEnabled { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:) @@ -159,8 +192,7 @@ [[NSNotificationCenter defaultCenter] removeObserver:self]; } -- (void)setRepeat:(BOOL)repeat -{ +- (void)setRepeat:(BOOL)repeat { if (repeat) { [self setRepeatEnabled]; } else { @@ -168,20 +200,17 @@ } } -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex -{ +- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex { RCTLogError(@"video cannot have any subviews"); return; } -- (void)removeReactSubview:(UIView *)subview -{ +- (void)removeReactSubview:(UIView *)subview { RCTLogError(@"video cannot have any subviews"); return; } -- (void)layoutSubviews -{ +- (void)layoutSubviews { [super layoutSubviews]; _playerLayer.frame = self.bounds; } @@ -194,6 +223,7 @@ _player = nil; _prevProgressUpdateTime = nil; _eventDispatcher = nil; + [_playerItem removeObserver:self forKeyPath:statusKeyPath]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } diff --git a/RCTVideoManager.m b/RCTVideoManager.m index 27fa5209..d67022e2 100644 --- a/RCTVideoManager.m +++ b/RCTVideoManager.m @@ -17,16 +17,22 @@ - (NSDictionary *)customDirectEventTypes { return @{ - @"videoLoaded": @{ + RNVideoLoadingEvent: @{ + @"registrationName": @"onLoadStart" + }, + RNVideoLoadedEvent: @{ @"registrationName": @"onLoad" }, - @"videoProgress": @{ + RNVideoLoadingErrorEvent: @{ + @"registrationName": @"onError" + }, + RNVideoProgressEvent: @{ @"registrationName": @"onProgress" }, }; } -RCT_EXPORT_VIEW_PROPERTY(src, NSString); +RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString); RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL); RCT_EXPORT_VIEW_PROPERTY(paused, BOOL); diff --git a/Video.ios.js b/Video.ios.js index 1647ccad..1c8ab5aa 100644 --- a/Video.ios.js +++ b/Video.ios.js @@ -10,18 +10,21 @@ var VideoStylePropTypes = require('./VideoStylePropTypes'); var NativeMethodsMixin = require('NativeMethodsMixin'); var flattenStyle = require('flattenStyle'); var merge = require('merge'); +var deepDiffer = require('deepDiffer'); var Video = React.createClass({ propTypes: { - source: PropTypes.string, style: StyleSheetPropType(VideoStylePropTypes), + source: PropTypes.object, resizeMode: PropTypes.string, repeat: PropTypes.bool, paused: PropTypes.bool, muted: PropTypes.bool, volume: PropTypes.number, rate: PropTypes.number, + onLoadStart: PropTypes.func, onLoad: PropTypes.func, + onError: PropTypes.func, onProgress: PropTypes.func, }, @@ -32,10 +35,18 @@ var Video = React.createClass({ validAttributes: ReactIOSViewAttributes.UIView }, + _onLoadStart(event) { + this.props.onLoadStart && this.props.onLoadStart(event.nativeEvent); + }, + _onLoad(event) { this.props.onLoad && this.props.onLoad(event.nativeEvent); }, + _onError(event) { + this.props.onError && this.props.onError(event.nativeEvent); + }, + _onProgress(event) { this.props.onProgress && this.props.onProgress(event.nativeEvent); }, @@ -43,6 +54,7 @@ var Video = React.createClass({ render() { var style = flattenStyle([styles.base, this.props.style]); var source = this.props.source; + var isNetwork = !!(source.uri && source.uri.match(/^https?:/)); var resizeMode; if (this.props.resizeMode === VideoResizeMode.stretch) { @@ -58,9 +70,14 @@ var Video = React.createClass({ var nativeProps = merge(this.props, { style, resizeMode: resizeMode, - src: source, + src: { + uri: source.uri, + isNetwork, + type: source.type || 'mp4' + }, onLoad: this._onLoad, onProgress: this._onProgress, + }); return @@ -69,7 +86,7 @@ var Video = React.createClass({ var RCTVideo = createReactIOSNativeComponentClass({ validAttributes: merge(ReactIOSViewAttributes.UIView, - {src: true, resizeMode: true, repeat: true, paused: true, muted: true, + {src: {diff: deepDiffer}, resizeMode: true, repeat: true, paused: true, muted: true, volume: true, rate: true}), uiViewClassName: 'RCTVideo', });