Add cache property; Make playerItemForSource in 'RCTVideoManager.m' async

This commit is contained in:
Laurin Quast 2018-02-28 17:42:49 +01:00
parent 6b2c1046dd
commit 438aa79494
3 changed files with 283 additions and 266 deletions

View File

@ -247,6 +247,7 @@ export default class Video extends Component {
Video.propTypes = {
/* Native only */
src: PropTypes.object,
cache: PropTypes.bool,
seek: PropTypes.number,
fullscreen: PropTypes.bool,
onVideoLoadStart: PropTypes.func,

View File

@ -89,13 +89,13 @@ static NSString *const timedMetadata = @"timedMetadata";
}
- (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem {
RCTVideoPlayerViewController* playerLayer= [[RCTVideoPlayerViewController alloc] init];
playerLayer.showsPlaybackControls = NO;
playerLayer.rctDelegate = self;
playerLayer.view.frame = self.bounds;
playerLayer.player = _player;
playerLayer.view.frame = self.bounds;
return playerLayer;
RCTVideoPlayerViewController* playerLayer= [[RCTVideoPlayerViewController alloc] init];
playerLayer.showsPlaybackControls = NO;
playerLayer.rctDelegate = self;
playerLayer.view.frame = self.bounds;
playerLayer.player = _player;
playerLayer.view.frame = self.bounds;
return playerLayer;
}
/* ---------------------------------------------------------
@ -104,35 +104,35 @@ static NSString *const timedMetadata = @"timedMetadata";
- (CMTime)playerItemDuration
{
AVPlayerItem *playerItem = [_player currentItem];
if (playerItem.status == AVPlayerItemStatusReadyToPlay)
{
return([playerItem duration]);
}
AVPlayerItem *playerItem = [_player currentItem];
if (playerItem.status == AVPlayerItemStatusReadyToPlay)
{
return([playerItem duration]);
}
return(kCMTimeInvalid);
return(kCMTimeInvalid);
}
- (CMTimeRange)playerItemSeekableTimeRange
{
AVPlayerItem *playerItem = [_player currentItem];
if (playerItem.status == AVPlayerItemStatusReadyToPlay)
{
return [playerItem seekableTimeRanges].firstObject.CMTimeRangeValue;
}
AVPlayerItem *playerItem = [_player currentItem];
if (playerItem.status == AVPlayerItemStatusReadyToPlay)
{
return [playerItem seekableTimeRanges].firstObject.CMTimeRangeValue;
}
return (kCMTimeRangeZero);
return (kCMTimeRangeZero);
}
/* Cancels the previously registered time observer. */
-(void)removePlayerTimeObserver
{
if (_timeObserver)
{
[_player removeTimeObserver:_timeObserver];
_timeObserver = nil;
}
if (_timeObserver)
{
[_player removeTimeObserver:_timeObserver];
_timeObserver = nil;
}
}
#pragma mark - Progress
@ -175,32 +175,32 @@ static NSString *const timedMetadata = @"timedMetadata";
- (void)sendProgressUpdate
{
AVPlayerItem *video = [_player currentItem];
if (video == nil || video.status != AVPlayerItemStatusReadyToPlay) {
return;
}
AVPlayerItem *video = [_player currentItem];
if (video == nil || video.status != AVPlayerItemStatusReadyToPlay) {
return;
}
CMTime playerDuration = [self playerItemDuration];
if (CMTIME_IS_INVALID(playerDuration)) {
return;
}
CMTime playerDuration = [self playerItemDuration];
if (CMTIME_IS_INVALID(playerDuration)) {
return;
}
CMTime currentTime = _player.currentTime;
const Float64 duration = CMTimeGetSeconds(playerDuration);
const Float64 currentTimeSecs = CMTimeGetSeconds(currentTime);
CMTime currentTime = _player.currentTime;
const Float64 duration = CMTimeGetSeconds(playerDuration);
const Float64 currentTimeSecs = CMTimeGetSeconds(currentTime);
[[NSNotificationCenter defaultCenter] postNotificationName:@"RCTVideo_progress" object:nil userInfo:@{@"progress": [NSNumber numberWithDouble: currentTimeSecs / duration]}];
[[NSNotificationCenter defaultCenter] postNotificationName:@"RCTVideo_progress" object:nil userInfo:@{@"progress": [NSNumber numberWithDouble: currentTimeSecs / duration]}];
if( currentTimeSecs >= 0 && self.onVideoProgress) {
self.onVideoProgress(@{
@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(currentTime)],
@"playableDuration": [self calculatePlayableDuration],
@"atValue": [NSNumber numberWithLongLong:currentTime.value],
@"atTimescale": [NSNumber numberWithInt:currentTime.timescale],
@"target": self.reactTag,
@"seekableDuration": [self calculateSeekableDuration],
});
}
if( currentTimeSecs >= 0 && self.onVideoProgress) {
self.onVideoProgress(@{
@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(currentTime)],
@"playableDuration": [self calculatePlayableDuration],
@"atValue": [NSNumber numberWithLongLong:currentTime.value],
@"atTimescale": [NSNumber numberWithInt:currentTime.timescale],
@"target": self.reactTag,
@"seekableDuration": [self calculateSeekableDuration],
});
}
}
/*!
@ -230,12 +230,12 @@ static NSString *const timedMetadata = @"timedMetadata";
- (NSNumber *)calculateSeekableDuration
{
CMTimeRange timeRange = [self playerItemSeekableTimeRange];
if (CMTIME_IS_NUMERIC(timeRange.duration))
{
return [NSNumber numberWithFloat:CMTimeGetSeconds(timeRange.duration)];
}
return [NSNumber numberWithInteger:0];
CMTimeRange timeRange = [self playerItemSeekableTimeRange];
if (CMTIME_IS_NUMERIC(timeRange.duration))
{
return [NSNumber numberWithFloat:CMTimeGetSeconds(timeRange.duration)];
}
return [NSNumber numberWithInteger:0];
}
- (void)addPlayerItemObservers
@ -266,11 +266,23 @@ static NSString *const timedMetadata = @"timedMetadata";
#pragma mark - Player and source
- (void)setCache:(BOOL *)cache
{
// @TODO: Implement
}
- (void)setSrc:(NSDictionary *)source
{
[self removePlayerTimeObserver];
[self removePlayerItemObservers];
_playerItem = [self playerItemForSource:source];
[self playerItemForSource:source withCallback:^(AVPlayerItem * playerItem) {
[self didSetPlayerItemWithSource:source playerItem:playerItem];
}];
}
- (void) didSetPlayerItemWithSource:(NSDictionary *)source playerItem:(AVPlayerItem *) playerItem
{
_playerItem = playerItem;
[self addPlayerItemObservers];
[_player pause];
@ -303,16 +315,16 @@ static NSString *const timedMetadata = @"timedMetadata";
id uri = [source objectForKey:@"uri"];
id type = [source objectForKey:@"type"];
self.onVideoLoadStart(@{@"src": @{
@"uri": uri ? uri : [NSNull null],
@"type": type ? type : [NSNull null],
@"isNetwork": [NSNumber numberWithBool:(bool)[source objectForKey:@"isNetwork"]]},
@"target": self.reactTag
});
@"uri": uri ? uri : [NSNull null],
@"type": type ? type : [NSNull null],
@"isNetwork": [NSNumber numberWithBool:(bool)[source objectForKey:@"isNetwork"]]},
@"target": self.reactTag
});
}
});
}
- (AVPlayerItem*)playerItemForSource:(NSDictionary *)source
- (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlayerItem *))handler
{
bool isNetwork = [RCTConvert BOOL:[source objectForKey:@"isNetwork"]];
bool isAsset = [RCTConvert BOOL:[source objectForKey:@"isAsset"]];
@ -320,52 +332,55 @@ static NSString *const timedMetadata = @"timedMetadata";
NSString *type = [source objectForKey:@"type"];
NSURL *url = (isNetwork || isAsset) ?
[NSURL URLWithString:uri] :
[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]];
[NSURL URLWithString:uri] :
[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]];
if (isNetwork) {
// @TODO: Check if item is cached an if so use the cached asset
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:@{AVURLAssetHTTPCookiesKey : cookies}];
return [AVPlayerItem playerItemWithAsset:asset];
handler([AVPlayerItem playerItemWithAsset:asset]);
return;
}
else if (isAsset) {
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
return [AVPlayerItem playerItemWithAsset:asset];
handler([AVPlayerItem playerItemWithAsset:asset]);
return;
}
return [AVPlayerItem playerItemWithURL:url];
handler([AVPlayerItem playerItemWithURL:url]);
return;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == _playerItem) {
if (object == _playerItem) {
// When timeMetadata is read the event onTimedMetadata is triggered
if ([keyPath isEqualToString: timedMetadata])
{
NSArray<AVMetadataItem *> *items = [change objectForKey:@"new"];
if (items && ![items isEqual:[NSNull null]] && items.count > 0) {
NSArray<AVMetadataItem *> *items = [change objectForKey:@"new"];
if (items && ![items isEqual:[NSNull null]] && items.count > 0) {
NSMutableArray *array = [NSMutableArray new];
for (AVMetadataItem *item in items) {
NSMutableArray *array = [NSMutableArray new];
for (AVMetadataItem *item in items) {
NSString *value = item.value;
NSString *identifier = item.identifier;
NSString *value = item.value;
NSString *identifier = item.identifier;
if (![value isEqual: [NSNull null]]) {
NSDictionary *dictionary = [[NSDictionary alloc] initWithObjects:@[value, identifier] forKeys:@[@"value", @"identifier"]];
if (![value isEqual: [NSNull null]]) {
NSDictionary *dictionary = [[NSDictionary alloc] initWithObjects:@[value, identifier] forKeys:@[@"value", @"identifier"]];
[array addObject:dictionary];
}
}
self.onTimedMetadata(@{
@"target": self.reactTag,
@"metadata": array
});
[array addObject:dictionary];
}
}
self.onTimedMetadata(@{
@"target": self.reactTag,
@"metadata": array
});
}
}
if ([keyPath isEqualToString:statusKeyPath]) {
@ -388,15 +403,15 @@ static NSString *const timedMetadata = @"timedMetadata";
CGAffineTransform preferredTransform = [videoTrack preferredTransform];
if ((videoTrack.naturalSize.width == preferredTransform.tx
&& videoTrack.naturalSize.height == preferredTransform.ty)
|| (preferredTransform.tx == 0 && preferredTransform.ty == 0))
&& videoTrack.naturalSize.height == preferredTransform.ty)
|| (preferredTransform.tx == 0 && preferredTransform.ty == 0))
{
orientation = @"landscape";
} else
orientation = @"portrait";
}
if(self.onVideoLoad) {
if(self.onVideoLoad) {
self.onVideoLoad(@{@"duration": [NSNumber numberWithFloat:duration],
@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(_playerItem.currentTime)],
@"canPlayReverse": [NSNumber numberWithBool:_playerItem.canPlayReverse],
@ -406,12 +421,12 @@ static NSString *const timedMetadata = @"timedMetadata";
@"canStepBackward": [NSNumber numberWithBool:_playerItem.canStepBackward],
@"canStepForward": [NSNumber numberWithBool:_playerItem.canStepForward],
@"naturalSize": @{
@"width": width,
@"height": height,
@"orientation": orientation
},
@"width": width,
@"height": height,
@"orientation": orientation
},
@"target": self.reactTag});
}
}
[self attachListeners];
@ -419,7 +434,7 @@ static NSString *const timedMetadata = @"timedMetadata";
} else if(_playerItem.status == AVPlayerItemStatusFailed && self.onVideoError) {
self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: _playerItem.error.code],
@"domain": _playerItem.error.domain},
@"target": self.reactTag});
@"target": self.reactTag});
}
} else if ([keyPath isEqualToString:playbackBufferEmptyKeyPath]) {
_playerBufferEmpty = YES;
@ -432,28 +447,28 @@ static NSString *const timedMetadata = @"timedMetadata";
_playerBufferEmpty = NO;
self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag});
}
} else if (object == _playerLayer) {
if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey]) {
if([change objectForKey:NSKeyValueChangeNewKey] && self.onReadyForDisplay) {
self.onReadyForDisplay(@{@"target": self.reactTag});
}
} else if (object == _playerLayer) {
if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey]) {
if([change objectForKey:NSKeyValueChangeNewKey] && self.onReadyForDisplay) {
self.onReadyForDisplay(@{@"target": self.reactTag});
}
}
} else if (object == _player) {
if([keyPath isEqualToString:playbackRate]) {
if(self.onPlaybackRateChange) {
self.onPlaybackRateChange(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate],
@"target": self.reactTag});
}
if(_playbackStalled && _player.rate > 0) {
if(self.onPlaybackResume) {
self.onPlaybackResume(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate],
@"target": self.reactTag});
}
_playbackStalled = NO;
}
if([keyPath isEqualToString:playbackRate]) {
if(self.onPlaybackRateChange) {
self.onPlaybackRateChange(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate],
@"target": self.reactTag});
}
if(_playbackStalled && _player.rate > 0) {
if(self.onPlaybackResume) {
self.onPlaybackResume(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate],
@"target": self.reactTag});
}
_playbackStalled = NO;
}
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
@ -481,7 +496,7 @@ static NSString *const timedMetadata = @"timedMetadata";
- (void)playerItemDidReachEnd:(NSNotification *)notification
{
if(self.onVideoEnd) {
self.onVideoEnd(@{@"target": self.reactTag});
self.onVideoEnd(@{@"target": self.reactTag});
}
if (_repeat) {
@ -569,9 +584,9 @@ static NSString *const timedMetadata = @"timedMetadata";
[_player seekToTime:cmSeekTime toleranceBefore:tolerance toleranceAfter:tolerance completionHandler:^(BOOL finished) {
if (!wasPaused) [_player play];
if(self.onVideoSeek) {
self.onVideoSeek(@{@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(item.currentTime)],
@"seekTime": [NSNumber numberWithFloat:seekTime],
@"target": self.reactTag});
self.onVideoSeek(@{@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(item.currentTime)],
@"seekTime": [NSNumber numberWithFloat:seekTime],
@"target": self.reactTag});
}
}];
@ -625,103 +640,103 @@ static NSString *const timedMetadata = @"timedMetadata";
- (BOOL)getFullscreen
{
return _fullscreenPlayerPresented;
return _fullscreenPlayerPresented;
}
- (void)setFullscreen:(BOOL)fullscreen
{
if( fullscreen && !_fullscreenPlayerPresented )
if( fullscreen && !_fullscreenPlayerPresented )
{
// Ensure player view controller is not null
if( !_playerViewController )
{
// Ensure player view controller is not null
if( !_playerViewController )
{
[self usePlayerViewController];
}
// Set presentation style to fullscreen
[_playerViewController setModalPresentationStyle:UIModalPresentationFullScreen];
[self usePlayerViewController];
}
// Set presentation style to fullscreen
[_playerViewController setModalPresentationStyle:UIModalPresentationFullScreen];
// Find the nearest view controller
UIViewController *viewController = [self firstAvailableUIViewController];
if( !viewController )
{
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
viewController = keyWindow.rootViewController;
if( viewController.childViewControllers.count > 0 )
{
viewController = viewController.childViewControllers.lastObject;
}
}
if( viewController )
{
_presentingViewController = viewController;
if(self.onVideoFullscreenPlayerWillPresent) {
self.onVideoFullscreenPlayerWillPresent(@{@"target": self.reactTag});
}
[viewController presentViewController:_playerViewController animated:true completion:^{
_playerViewController.showsPlaybackControls = YES;
_fullscreenPlayerPresented = fullscreen;
if(self.onVideoFullscreenPlayerDidPresent) {
self.onVideoFullscreenPlayerDidPresent(@{@"target": self.reactTag});
}
}];
}
}
else if ( !fullscreen && _fullscreenPlayerPresented )
// Find the nearest view controller
UIViewController *viewController = [self firstAvailableUIViewController];
if( !viewController )
{
[self videoPlayerViewControllerWillDismiss:_playerViewController];
[_presentingViewController dismissViewControllerAnimated:true completion:^{
[self videoPlayerViewControllerDidDismiss:_playerViewController];
}];
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
viewController = keyWindow.rootViewController;
if( viewController.childViewControllers.count > 0 )
{
viewController = viewController.childViewControllers.lastObject;
}
}
if( viewController )
{
_presentingViewController = viewController;
if(self.onVideoFullscreenPlayerWillPresent) {
self.onVideoFullscreenPlayerWillPresent(@{@"target": self.reactTag});
}
[viewController presentViewController:_playerViewController animated:true completion:^{
_playerViewController.showsPlaybackControls = YES;
_fullscreenPlayerPresented = fullscreen;
if(self.onVideoFullscreenPlayerDidPresent) {
self.onVideoFullscreenPlayerDidPresent(@{@"target": self.reactTag});
}
}];
}
}
else if ( !fullscreen && _fullscreenPlayerPresented )
{
[self videoPlayerViewControllerWillDismiss:_playerViewController];
[_presentingViewController dismissViewControllerAnimated:true completion:^{
[self videoPlayerViewControllerDidDismiss:_playerViewController];
}];
}
}
- (void)usePlayerViewController
{
if( _player )
{
_playerViewController = [self createPlayerViewController:_player withPlayerItem:_playerItem];
// to prevent video from being animated when resizeMode is 'cover'
// resize mode must be set before subview is added
[self setResizeMode:_resizeMode];
[self addSubview:_playerViewController.view];
}
if( _player )
{
_playerViewController = [self createPlayerViewController:_player withPlayerItem:_playerItem];
// to prevent video from being animated when resizeMode is 'cover'
// resize mode must be set before subview is added
[self setResizeMode:_resizeMode];
[self addSubview:_playerViewController.view];
}
}
- (void)usePlayerLayer
{
if( _player )
{
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
_playerLayer.frame = self.bounds;
_playerLayer.needsDisplayOnBoundsChange = YES;
if( _player )
{
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
_playerLayer.frame = self.bounds;
_playerLayer.needsDisplayOnBoundsChange = YES;
// to prevent video from being animated when resizeMode is 'cover'
// resize mode must be set before layer is added
[self setResizeMode:_resizeMode];
[_playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil];
// to prevent video from being animated when resizeMode is 'cover'
// resize mode must be set before layer is added
[self setResizeMode:_resizeMode];
[_playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil];
[self.layer addSublayer:_playerLayer];
self.layer.needsDisplayOnBoundsChange = YES;
}
[self.layer addSublayer:_playerLayer];
self.layer.needsDisplayOnBoundsChange = YES;
}
}
- (void)setControls:(BOOL)controls
{
if( _controls != controls || (!_playerLayer && !_playerViewController) )
if( _controls != controls || (!_playerLayer && !_playerViewController) )
{
_controls = controls;
if( _controls )
{
_controls = controls;
if( _controls )
{
[self removePlayerLayer];
[self usePlayerViewController];
}
else
{
[_playerViewController.view removeFromSuperview];
_playerViewController = nil;
[self usePlayerLayer];
}
[self removePlayerLayer];
[self usePlayerViewController];
}
else
{
[_playerViewController.view removeFromSuperview];
_playerViewController = nil;
[self usePlayerLayer];
}
}
}
- (void)setProgressUpdateInterval:(float)progressUpdateInterval
@ -731,33 +746,33 @@ static NSString *const timedMetadata = @"timedMetadata";
- (void)removePlayerLayer
{
[_playerLayer removeFromSuperlayer];
[_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath];
_playerLayer = nil;
[_playerLayer removeFromSuperlayer];
[_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath];
_playerLayer = nil;
}
#pragma mark - RCTVideoPlayerViewControllerDelegate
- (void)videoPlayerViewControllerWillDismiss:(AVPlayerViewController *)playerViewController
{
if (_playerViewController == playerViewController && _fullscreenPlayerPresented && self.onVideoFullscreenPlayerWillDismiss)
{
self.onVideoFullscreenPlayerWillDismiss(@{@"target": self.reactTag});
}
if (_playerViewController == playerViewController && _fullscreenPlayerPresented && self.onVideoFullscreenPlayerWillDismiss)
{
self.onVideoFullscreenPlayerWillDismiss(@{@"target": self.reactTag});
}
}
- (void)videoPlayerViewControllerDidDismiss:(AVPlayerViewController *)playerViewController
{
if (_playerViewController == playerViewController && _fullscreenPlayerPresented)
{
_fullscreenPlayerPresented = false;
_presentingViewController = nil;
_playerViewController = nil;
[self applyModifiers];
if(self.onVideoFullscreenPlayerDidDismiss) {
self.onVideoFullscreenPlayerDidDismiss(@{@"target": self.reactTag});
}
if (_playerViewController == playerViewController && _fullscreenPlayerPresented)
{
_fullscreenPlayerPresented = false;
_presentingViewController = nil;
_playerViewController = nil;
[self applyModifiers];
if(self.onVideoFullscreenPlayerDidDismiss) {
self.onVideoFullscreenPlayerDidDismiss(@{@"target": self.reactTag});
}
}
}
#pragma mark - React View Management
@ -773,12 +788,12 @@ static NSString *const timedMetadata = @"timedMetadata";
if( _controls )
{
view.frame = self.bounds;
[_playerViewController.contentOverlayView insertSubview:view atIndex:atIndex];
view.frame = self.bounds;
[_playerViewController.contentOverlayView insertSubview:view atIndex:atIndex];
}
else
{
RCTLogError(@"video cannot have any subviews");
RCTLogError(@"video cannot have any subviews");
}
return;
}
@ -787,7 +802,7 @@ static NSString *const timedMetadata = @"timedMetadata";
{
if( _controls )
{
[subview removeFromSuperview];
[subview removeFromSuperview];
}
else
{
@ -810,10 +825,10 @@ static NSString *const timedMetadata = @"timedMetadata";
}
else
{
[CATransaction begin];
[CATransaction setAnimationDuration:0];
_playerLayer.frame = self.bounds;
[CATransaction commit];
[CATransaction begin];
[CATransaction setAnimationDuration:0];
_playerLayer.frame = self.bounds;
[CATransaction commit];
}
}

View File

@ -20,6 +20,7 @@ RCT_EXPORT_MODULE();
}
RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(cache, BOOL);
RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString);
RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL);
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL);