react-native-video/ios/RCTVideo.m
Dom DiCicco 98c51f114b issue 310 - Added a new property, ignoreSilentSwitch. (#403)
* issue 310 - Added a new property, ignoreSilentSwitch. When true, audio will play even when the silent switch on an iOS device is set to silent. When false, the audio will toggle with the silent switch. Sets the AVAudioSession to either playback or ambient.

* Added ignoreSilentSwitch usage to example app and to readme

* Changed ignoreSilentSwitch to accept two string values, ignore and obey.
This accounts for the case where the user does not want to modify the audio session from a particular video instance. The user would not use the ignoreSilentSwitch property at all in that case.
Also, the audio session will only be updated when the video is unpaused, instead of whenever the video component has updated props. This allows for multiple videos to be on the screen, with the most recent video unpaused (aka played) being the video that has control over the audio session.
2017-04-20 14:10:06 -04:00

829 lines
27 KiB
Objective-C

#import <React/RCTConvert.h>
#import "RCTVideo.h"
#import <React/RCTBridgeModule.h>
#import <React/RCTEventDispatcher.h>
#import <React/UIView+React.h>
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";
static NSString *const timedMetadata = @"timedMetadata";
@implementation RCTVideo
{
AVPlayer *_player;
AVPlayerItem *_playerItem;
BOOL _playerItemObserversSet;
BOOL _playerBufferEmpty;
AVPlayerLayer *_playerLayer;
AVPlayerViewController *_playerViewController;
NSURL *_videoURL;
/* Required to publish events */
RCTEventDispatcher *_eventDispatcher;
BOOL _playbackRateObserverRegistered;
bool _pendingSeek;
float _pendingSeekTime;
float _lastSeekTime;
/* For sending videoProgress events */
Float64 _progressUpdateInterval;
BOOL _controls;
id _timeObserver;
/* Keep track of any modifiers, need to be applied after each play */
float _volume;
float _rate;
BOOL _muted;
BOOL _paused;
BOOL _repeat;
BOOL _playbackStalled;
BOOL _playInBackground;
BOOL _playWhenInactive;
NSString * _ignoreSilentSwitch;
NSString * _resizeMode;
BOOL _fullscreenPlayerPresented;
UIViewController * _presentingViewController;
}
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
{
if ((self = [super init])) {
_eventDispatcher = eventDispatcher;
_playbackRateObserverRegistered = NO;
_playbackStalled = NO;
_rate = 1.0;
_volume = 1.0;
_resizeMode = @"AVLayerVideoGravityResizeAspectFill";
_pendingSeek = false;
_pendingSeekTime = 0.0f;
_lastSeekTime = 0.0f;
_progressUpdateInterval = 250;
_controls = NO;
_playerBufferEmpty = YES;
_playInBackground = false;
_playWhenInactive = false;
_ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillResignActive:)
name:UIApplicationWillResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillEnterForeground:)
name:UIApplicationWillEnterForegroundNotification
object:nil];
}
return self;
}
- (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;
}
/* ---------------------------------------------------------
** Get the duration for a AVPlayerItem.
** ------------------------------------------------------- */
- (CMTime)playerItemDuration
{
AVPlayerItem *playerItem = [_player currentItem];
if (playerItem.status == AVPlayerItemStatusReadyToPlay)
{
return([playerItem duration]);
}
return(kCMTimeInvalid);
}
- (CMTimeRange)playerItemSeekableTimeRange
{
AVPlayerItem *playerItem = [_player currentItem];
if (playerItem.status == AVPlayerItemStatusReadyToPlay)
{
return [playerItem seekableTimeRanges].firstObject.CMTimeRangeValue;
}
return (kCMTimeRangeZero);
}
/* Cancels the previously registered time observer. */
-(void)removePlayerTimeObserver
{
if (_timeObserver)
{
[_player removeTimeObserver:_timeObserver];
_timeObserver = nil;
}
}
#pragma mark - Progress
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self removePlayerItemObservers];
[_player removeObserver:self forKeyPath:playbackRate context:nil];
}
#pragma mark - App lifecycle handlers
- (void)applicationWillResignActive:(NSNotification *)notification
{
if (_playInBackground || _playWhenInactive || _paused) return;
[_player pause];
[_player setRate:0.0];
}
- (void)applicationDidEnterBackground:(NSNotification *)notification
{
if (_playInBackground) {
// Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html
[_playerLayer setPlayer:nil];
}
}
- (void)applicationWillEnterForeground:(NSNotification *)notification
{
[self applyModifiers];
if (_playInBackground) {
[_playerLayer setPlayer:_player];
}
}
#pragma mark - Progress
- (void)sendProgressUpdate
{
AVPlayerItem *video = [_player currentItem];
if (video == nil || video.status != AVPlayerItemStatusReadyToPlay) {
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);
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": [NSNumber numberWithFloat:CMTimeGetSeconds([self playerItemSeekableTimeRange].duration)],
});
}
}
/*!
* Calculates and returns the playable duration of the current player item using its loaded time ranges.
*
* \returns The playable duration of the current player item in seconds.
*/
- (NSNumber *)calculatePlayableDuration
{
AVPlayerItem *video = _player.currentItem;
if (video.status == AVPlayerItemStatusReadyToPlay) {
__block CMTimeRange effectiveTimeRange;
[video.loadedTimeRanges enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
CMTimeRange timeRange = [obj CMTimeRangeValue];
if (CMTimeRangeContainsTime(timeRange, video.currentTime)) {
effectiveTimeRange = timeRange;
*stop = YES;
}
}];
Float64 playableDuration = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange));
if (playableDuration > 0) {
return [NSNumber numberWithFloat:playableDuration];
}
}
return [NSNumber numberWithInteger:0];
}
- (void)addPlayerItemObservers
{
[_playerItem addObserver:self forKeyPath:statusKeyPath options:0 context:nil];
[_playerItem addObserver:self forKeyPath:playbackBufferEmptyKeyPath options:0 context:nil];
[_playerItem addObserver:self forKeyPath:playbackLikelyToKeepUpKeyPath options:0 context:nil];
[_playerItem addObserver:self forKeyPath:timedMetadata options:NSKeyValueObservingOptionNew context:nil];
_playerItemObserversSet = YES;
}
/* Fixes https://github.com/brentvatne/react-native-video/issues/43
* Crashes caused when trying to remove the observer when there is no
* observer set */
- (void)removePlayerItemObservers
{
if (_playerItemObserversSet) {
[_playerItem removeObserver:self forKeyPath:statusKeyPath];
[_playerItem removeObserver:self forKeyPath:playbackBufferEmptyKeyPath];
[_playerItem removeObserver:self forKeyPath:playbackLikelyToKeepUpKeyPath];
[_playerItem removeObserver:self forKeyPath:timedMetadata];
_playerItemObserversSet = NO;
}
}
#pragma mark - Player and source
- (void)setSrc:(NSDictionary *)source
{
[self removePlayerTimeObserver];
[self removePlayerItemObservers];
_playerItem = [self playerItemForSource:source];
[self addPlayerItemObservers];
[_player pause];
[self removePlayerLayer];
[_playerViewController.view removeFromSuperview];
_playerViewController = nil;
if (_playbackRateObserverRegistered) {
[_player removeObserver:self forKeyPath:playbackRate context:nil];
_playbackRateObserverRegistered = NO;
}
_player = [AVPlayer playerWithPlayerItem:_playerItem];
_player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
[_player addObserver:self forKeyPath:playbackRate options:0 context:nil];
_playbackRateObserverRegistered = YES;
const Float64 progressUpdateIntervalMS = _progressUpdateInterval / 1000;
// @see endScrubbing in AVPlayerDemoPlaybackViewController.m of https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Introduction/Intro.html
__weak RCTVideo *weakSelf = self;
_timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(progressUpdateIntervalMS, NSEC_PER_SEC)
queue:NULL
usingBlock:^(CMTime time) { [weakSelf sendProgressUpdate]; }
];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//Perform on next run loop, otherwise onVideoLoadStart is nil
if(self.onVideoLoadStart) {
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
});
}
});
}
- (AVPlayerItem*)playerItemForSource:(NSDictionary *)source
{
bool isNetwork = [RCTConvert BOOL:[source objectForKey:@"isNetwork"]];
bool isAsset = [RCTConvert BOOL:[source objectForKey:@"isAsset"]];
NSString *uri = [source objectForKey:@"uri"];
NSString *type = [source objectForKey:@"type"];
NSURL *url = (isNetwork || isAsset) ?
[NSURL URLWithString:uri] :
[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]];
if (isNetwork) {
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:@{AVURLAssetHTTPCookiesKey : cookies}];
return [AVPlayerItem playerItemWithAsset:asset];
}
else if (isAsset) {
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
return [AVPlayerItem playerItemWithAsset:asset];
}
return [AVPlayerItem playerItemWithURL:url];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
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) {
NSMutableArray *array = [NSMutableArray new];
for (AVMetadataItem *item in items) {
NSString *value = item.value;
NSString *identifier = item.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
});
}
}
if ([keyPath isEqualToString:statusKeyPath]) {
// Handle player item status change.
if (_playerItem.status == AVPlayerItemStatusReadyToPlay) {
float duration = CMTimeGetSeconds(_playerItem.asset.duration);
if (isnan(duration)) {
duration = 0.0;
}
NSObject *width = @"undefined";
NSObject *height = @"undefined";
NSString *orientation = @"undefined";
if ([_playerItem.asset tracksWithMediaType:AVMediaTypeVideo].count > 0) {
AVAssetTrack *videoTrack = [[_playerItem.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
width = [NSNumber numberWithFloat:videoTrack.naturalSize.width];
height = [NSNumber numberWithFloat:videoTrack.naturalSize.height];
CGAffineTransform preferredTransform = [videoTrack preferredTransform];
if ((videoTrack.naturalSize.width == preferredTransform.tx
&& videoTrack.naturalSize.height == preferredTransform.ty)
|| (preferredTransform.tx == 0 && preferredTransform.ty == 0))
{
orientation = @"landscape";
} else
orientation = @"portrait";
}
if(self.onVideoLoad) {
self.onVideoLoad(@{@"duration": [NSNumber numberWithFloat: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],
@"naturalSize": @{
@"width": width,
@"height": height,
@"orientation": orientation
},
@"target": self.reactTag});
}
[self attachListeners];
[self applyModifiers];
} else if(_playerItem.status == AVPlayerItemStatusFailed && self.onVideoError) {
self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: _playerItem.error.code],
@"domain": _playerItem.error.domain},
@"target": self.reactTag});
}
} else if ([keyPath isEqualToString:playbackBufferEmptyKeyPath]) {
_playerBufferEmpty = YES;
self.onVideoBuffer(@{@"isBuffering": @(YES), @"target": self.reactTag});
} else if ([keyPath isEqualToString:playbackLikelyToKeepUpKeyPath]) {
// Continue playing (or not if paused) after being paused due to hitting an unbuffered zone.
if ((!(_controls || _fullscreenPlayerPresented) || _playerBufferEmpty) && _playerItem.playbackLikelyToKeepUp) {
[self setPaused:_paused];
}
_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 == _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;
}
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)attachListeners
{
// listen for end of file
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:[_player currentItem]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playbackStalled:)
name:AVPlayerItemPlaybackStalledNotification
object:nil];
}
- (void)playbackStalled:(NSNotification *)notification
{
if(self.onPlaybackStalled) {
self.onPlaybackStalled(@{@"target": self.reactTag});
}
_playbackStalled = YES;
}
- (void)playerItemDidReachEnd:(NSNotification *)notification
{
if(self.onVideoEnd) {
self.onVideoEnd(@{@"target": self.reactTag});
}
if (_repeat) {
AVPlayerItem *item = [notification object];
[item seekToTime:kCMTimeZero];
[self applyModifiers];
}
}
#pragma mark - Prop setters
- (void)setResizeMode:(NSString*)mode
{
if( _controls )
{
_playerViewController.videoGravity = mode;
}
else
{
_playerLayer.videoGravity = mode;
}
_resizeMode = mode;
}
- (void)setPlayInBackground:(BOOL)playInBackground
{
_playInBackground = playInBackground;
}
- (void)setPlayWhenInactive:(BOOL)playWhenInactive
{
_playWhenInactive = playWhenInactive;
}
- (void)setIgnoreSilentSwitch:(NSString *)ignoreSilentSwitch
{
_ignoreSilentSwitch = ignoreSilentSwitch;
[self applyModifiers];
}
- (void)setPaused:(BOOL)paused
{
if (paused) {
[_player pause];
[_player setRate:0.0];
} else {
if([_ignoreSilentSwitch isEqualToString:@"ignore"]) {
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
} else if([_ignoreSilentSwitch isEqualToString:@"obey"]) {
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil];
}
[_player play];
[_player setRate:_rate];
}
_paused = paused;
}
- (float)getCurrentTime
{
return _playerItem != NULL ? CMTimeGetSeconds(_playerItem.currentTime) : 0;
}
- (void)setCurrentTime:(float)currentTime
{
[self setSeek: currentTime];
}
- (void)setSeek:(float)seekTime
{
int timeScale = 10000;
AVPlayerItem *item = _player.currentItem;
if (item && item.status == AVPlayerItemStatusReadyToPlay) {
// TODO check loadedTimeRanges
CMTime cmSeekTime = CMTimeMakeWithSeconds(seekTime, timeScale);
CMTime current = item.currentTime;
// TODO figure out a good tolerance level
CMTime tolerance = CMTimeMake(1000, timeScale);
BOOL wasPaused = _paused;
if (CMTimeCompare(current, cmSeekTime) != 0) {
if (!wasPaused) [_player pause];
[_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});
}
}];
_pendingSeek = false;
}
} else {
// TODO: See if this makes sense and if so, actually implement it
_pendingSeek = true;
_pendingSeekTime = seekTime;
}
}
- (void)setRate:(float)rate
{
_rate = rate;
[self applyModifiers];
}
- (void)setMuted:(BOOL)muted
{
_muted = muted;
[self applyModifiers];
}
- (void)setVolume:(float)volume
{
_volume = volume;
[self applyModifiers];
}
- (void)applyModifiers
{
if (_muted) {
[_player setVolume:0];
[_player setMuted:YES];
} else {
[_player setVolume:_volume];
[_player setMuted:NO];
}
[self setResizeMode:_resizeMode];
[self setRepeat:_repeat];
[self setPaused:_paused];
[self setControls:_controls];
}
- (void)setRepeat:(BOOL)repeat {
_repeat = repeat;
}
- (BOOL)getFullscreen
{
return _fullscreenPlayerPresented;
}
- (void)setFullscreen:(BOOL)fullscreen
{
if( fullscreen && !_fullscreenPlayerPresented )
{
// Ensure player view controller is not null
if( !_playerViewController )
{
[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 )
{
[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];
}
}
- (void)usePlayerLayer
{
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];
[self.layer addSublayer:_playerLayer];
self.layer.needsDisplayOnBoundsChange = YES;
}
}
- (void)setControls:(BOOL)controls
{
if( _controls != controls || (!_playerLayer && !_playerViewController) )
{
_controls = controls;
if( _controls )
{
[self removePlayerLayer];
[self usePlayerViewController];
}
else
{
[_playerViewController.view removeFromSuperview];
_playerViewController = nil;
[self usePlayerLayer];
}
}
}
- (void)setProgressUpdateInterval:(float)progressUpdateInterval
{
_progressUpdateInterval = progressUpdateInterval;
}
- (void)removePlayerLayer
{
[_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});
}
}
- (void)videoPlayerViewControllerDidDismiss:(AVPlayerViewController *)playerViewController
{
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
- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
{
// We are early in the game and somebody wants to set a subview.
// That can only be in the context of playerViewController.
if( !_controls && !_playerLayer && !_playerViewController )
{
[self setControls:true];
}
if( _controls )
{
view.frame = self.bounds;
[_playerViewController.contentOverlayView insertSubview:view atIndex:atIndex];
}
else
{
RCTLogError(@"video cannot have any subviews");
}
return;
}
- (void)removeReactSubview:(UIView *)subview
{
if( _controls )
{
[subview removeFromSuperview];
}
else
{
RCTLogError(@"video cannot have any subviews");
}
return;
}
- (void)layoutSubviews
{
[super layoutSubviews];
if( _controls )
{
_playerViewController.view.frame = self.bounds;
// also adjust all subviews of contentOverlayView
for (UIView* subview in _playerViewController.contentOverlayView.subviews) {
subview.frame = self.bounds;
}
}
else
{
[CATransaction begin];
[CATransaction setAnimationDuration:0];
_playerLayer.frame = self.bounds;
[CATransaction commit];
}
}
#pragma mark - Lifecycle
- (void)removeFromSuperview
{
[_player pause];
if (_playbackRateObserverRegistered) {
[_player removeObserver:self forKeyPath:playbackRate context:nil];
_playbackRateObserverRegistered = NO;
}
_player = nil;
[self removePlayerLayer];
[_playerViewController.view removeFromSuperview];
_playerViewController = nil;
[self removePlayerTimeObserver];
[self removePlayerItemObservers];
_eventDispatcher = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super removeFromSuperview];
}
@end