From 0c03932adad3c800a062bd101fa5bf62ff651e48 Mon Sep 17 00:00:00 2001 From: Ash Mishra Date: Thu, 2 Aug 2018 10:32:50 -0700 Subject: [PATCH 01/43] Working on autorotation of video player --- ios/RCTVideo.m | 6 +++++- ios/RCTVideoPlayerViewController.m | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/ios/RCTVideo.m b/ios/RCTVideo.m index f5fc6125..a696987c 100644 --- a/ios/RCTVideo.m +++ b/ios/RCTVideo.m @@ -55,6 +55,7 @@ static int const RCTVideoUnset = -1; BOOL _playWhenInactive; NSString * _ignoreSilentSwitch; NSString * _resizeMode; + BOOL _fullscreen; BOOL _fullscreenPlayerPresented; UIViewController * _presentingViewController; } @@ -329,6 +330,8 @@ static int const RCTVideoUnset = -1; _playbackRateObserverRegistered = YES; [self addPlayerTimeObserver]; + + [self setFullscreen:_fullscreen]; //Perform on next run loop, otherwise onVideoLoadStart is nil if(self.onVideoLoadStart) { @@ -994,7 +997,8 @@ static int const RCTVideoUnset = -1; - (void)setFullscreen:(BOOL)fullscreen { - if( fullscreen && !_fullscreenPlayerPresented ) + _fullscreen = fullscreen; + if( fullscreen && !_fullscreenPlayerPresented && _player ) { // Ensure player view controller is not null if( !_playerViewController ) diff --git a/ios/RCTVideoPlayerViewController.m b/ios/RCTVideoPlayerViewController.m index 7809221a..12480d54 100644 --- a/ios/RCTVideoPlayerViewController.m +++ b/ios/RCTVideoPlayerViewController.m @@ -13,4 +13,18 @@ [_rctDelegate videoPlayerViewControllerDidDismiss:self]; } +- (BOOL)shouldAutorotate { + return YES; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + + return UIInterfaceOrientationMaskLandscape; +} + +- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { + + return UIInterfaceOrientationLandscapeLeft; +} + @end From 55e0e4dcfa7f67cdc28ea53738dff46344a9ed04 Mon Sep 17 00:00:00 2001 From: Charlie Martin Date: Sat, 4 Aug 2018 17:28:33 -0400 Subject: [PATCH 02/43] Remove flash between poster and video This resolves https://github.com/react-native-community/react-native-video/issues/1128 --- Video.js | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/Video.js b/Video.js index 5c6cd1d3..f351c3ff 100644 --- a/Video.js +++ b/Video.js @@ -248,35 +248,21 @@ export default class Video extends Component { onAudioBecomingNoisy: this._onAudioBecomingNoisy, }); - if (this.props.poster && this.state.showPoster) { - const posterStyle = { - position: 'absolute', - left: 0, - top: 0, - right: 0, - bottom: 0, - resizeMode: this.props.posterResizeMode || 'contain' - }; - - return ( - - - - - ); - } + const posterStyle = { + ...StyleSheet.absoluteFillObject, + resizeMode: this.props.posterResizeMode || 'contain', + }; return ( - + + + {this.props.poster && + this.state.showPoster && ( + + + + )} + ); } } From f41831ceacb1377c90649be3aaaaf3e6c9e36078 Mon Sep 17 00:00:00 2001 From: Ash Mishra Date: Thu, 9 Aug 2018 09:58:03 -0700 Subject: [PATCH 03/43] Added fullscreen options for iOS Player --- Video.js | 5 + ios/RCTVideo.m | 560 +++++++++++++++-------------- ios/RCTVideoManager.m | 3 +- ios/RCTVideoPlayerViewController.h | 5 + ios/RCTVideoPlayerViewController.m | 24 +- 5 files changed, 328 insertions(+), 269 deletions(-) diff --git a/Video.js b/Video.js index f32725f3..3d3a638a 100644 --- a/Video.js +++ b/Video.js @@ -289,6 +289,11 @@ Video.propTypes = { PropTypes.object ]), fullscreen: PropTypes.bool, + fullscreenOptions: PropTypes.shape({ + enabled: PropTypes.bool, + preferredOrientation: PropTypes.string, + autorotate: PropTypes.bool + }), onVideoLoadStart: PropTypes.func, onVideoLoad: PropTypes.func, onVideoBuffer: PropTypes.func, diff --git a/ios/RCTVideo.m b/ios/RCTVideo.m index a696987c..3adab702 100644 --- a/ios/RCTVideo.m +++ b/ios/RCTVideo.m @@ -25,21 +25,21 @@ static int const RCTVideoUnset = -1; BOOL _playerLayerObserverSet; AVPlayerViewController *_playerViewController; NSURL *_videoURL; - + /* Required to publish events */ RCTEventDispatcher *_eventDispatcher; BOOL _playbackRateObserverRegistered; BOOL _videoLoadStarted; - + 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; @@ -56,6 +56,7 @@ static int const RCTVideoUnset = -1; NSString * _ignoreSilentSwitch; NSString * _resizeMode; BOOL _fullscreen; + NSDictionary* _fullscreenOptions; BOOL _fullscreenPlayerPresented; UIViewController * _presentingViewController; } @@ -64,7 +65,7 @@ static int const RCTVideoUnset = -1; { if ((self = [super init])) { _eventDispatcher = eventDispatcher; - + _playbackRateObserverRegistered = NO; _playbackStalled = NO; _rate = 1.0; @@ -80,39 +81,46 @@ static int const RCTVideoUnset = -1; _allowsExternalPlayback = YES; _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]; - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChanged:) name:AVAudioSessionRouteChangeNotification object:nil]; } - + return self; } - (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem { - RCTVideoPlayerViewController* playerLayer= [[RCTVideoPlayerViewController alloc] init]; - playerLayer.showsPlaybackControls = YES; - 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 = YES; + playerLayer.rctDelegate = self; + + if (_fullscreenOptions) { + playerLayer.preferredOrientation = [RCTConvert NSString:[_fullscreenOptions objectForKey:@"preferredOrientation"]]; + playerLayer.autorotate = [RCTConvert BOOL:[_fullscreenOptions objectForKey:@"autorotate"]]; + } + + playerLayer.view.frame = self.bounds; + playerLayer.player = player; + playerLayer.view.frame = self.bounds; + return playerLayer; } /* --------------------------------------------------------- @@ -121,24 +129,24 @@ static int const RCTVideoUnset = -1; - (CMTime)playerItemDuration { - AVPlayerItem *playerItem = [_player currentItem]; - if (playerItem.status == AVPlayerItemStatusReadyToPlay) - { - return([playerItem duration]); - } - - return(kCMTimeInvalid); + 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); + AVPlayerItem *playerItem = [_player currentItem]; + if (playerItem.status == AVPlayerItemStatusReadyToPlay) + { + return [playerItem seekableTimeRanges].firstObject.CMTimeRangeValue; + } + + return (kCMTimeRangeZero); } -(void)addPlayerTimeObserver @@ -156,11 +164,11 @@ static int const RCTVideoUnset = -1; /* 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 @@ -178,7 +186,7 @@ static int const RCTVideoUnset = -1; - (void)applicationWillResignActive:(NSNotification *)notification { if (_playInBackground || _playWhenInactive || _paused) return; - + [_player pause]; [_player setRate:0.0]; } @@ -203,43 +211,43 @@ static int const RCTVideoUnset = -1; - (void)audioRouteChanged:(NSNotification *)notification { - NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; - NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; - if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { - self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); - } + NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; + NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; + if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { + self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); + } } #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); - - [[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], - }); - } + 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); + + [[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], + }); + } } /*! @@ -269,12 +277,12 @@ static int const RCTVideoUnset = -1; - (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 @@ -309,42 +317,42 @@ static int const RCTVideoUnset = -1; [self removePlayerItemObservers]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - + // perform on next run loop, otherwise other passed react-props may not be set _playerItem = [self playerItemForSource:source]; [self addPlayerItemObservers]; - + [_player pause]; [_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; - + [self addPlayerTimeObserver]; - [self setFullscreen:_fullscreen]; - + [self setFullscreenOptions:_fullscreenOptions]; + //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 - }); + @"uri": uri ? uri : [NSNull null], + @"type": type ? type : [NSNull null], + @"isNetwork": [NSNumber numberWithBool:(bool)[source objectForKey:@"isNetwork"]]}, + @"target": self.reactTag + }); } - + }); _videoLoadStarted = YES; } @@ -379,15 +387,15 @@ static int const RCTVideoUnset = -1; AVURLAsset *asset; NSMutableDictionary *assetOptions = [[NSMutableDictionary alloc] init]; - + if (isNetwork) { /* Per #1091, this is not a public API. We need to either get approval from Apple to use this * or use a different approach. - NSDictionary *headers = [source objectForKey:@"requestHeaders"]; - if ([headers count] > 0) { - [assetOptions setObject:headers forKey:@"AVURLAssetHTTPHeaderFieldsKey"]; - } - */ + NSDictionary *headers = [source objectForKey:@"requestHeaders"]; + if ([headers count] > 0) { + [assetOptions setObject:headers forKey:@"AVURLAssetHTTPHeaderFieldsKey"]; + } + */ NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; [assetOptions setObject:cookies forKey:AVURLAssetHTTPCookiesKey]; asset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:uri] options:assetOptions]; @@ -410,14 +418,14 @@ static int const RCTVideoUnset = -1; ofTrack:videoAsset atTime:kCMTimeZero error:nil]; - + AVAssetTrack *audioAsset = [asset tracksWithMediaType:AVMediaTypeAudio].firstObject; AVMutableCompositionTrack *audioCompTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; [audioCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) ofTrack:audioAsset atTime:kCMTimeZero error:nil]; - + NSMutableArray* validTextTracks = [NSMutableArray array]; for (int i = 0; i < _textTracks.count; ++i) { AVURLAsset *textURLAsset; @@ -434,14 +442,14 @@ static int const RCTVideoUnset = -1; addMutableTrackWithMediaType:AVMediaTypeText preferredTrackID:kCMPersistentTrackID_Invalid]; [textCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) - ofTrack:textTrackAsset - atTime:kCMTimeZero - error:nil]; + ofTrack:textTrackAsset + atTime:kCMTimeZero + error:nil]; } if (validTextTracks.count != _textTracks.count) { [self setTextTracks:validTextTracks]; } - + return [AVPlayerItem playerItemWithAsset:mixComposition]; } @@ -573,7 +581,7 @@ static int const RCTVideoUnset = -1; selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:[_player currentItem]]; - + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemPlaybackStalledNotification object:nil]; @@ -594,9 +602,9 @@ static int const RCTVideoUnset = -1; - (void)playerItemDidReachEnd:(NSNotification *)notification { if(self.onVideoEnd) { - self.onVideoEnd(@{@"target": self.reactTag}); + self.onVideoEnd(@{@"target": self.reactTag}); } - + if (_repeat) { AVPlayerItem *item = [notification object]; [item seekToTime:kCMTimeZero]; @@ -628,8 +636,8 @@ static int const RCTVideoUnset = -1; - (void)setAllowsExternalPlayback:(BOOL)allowsExternalPlayback { - _allowsExternalPlayback = allowsExternalPlayback; - _player.allowsExternalPlayback = _allowsExternalPlayback; + _allowsExternalPlayback = allowsExternalPlayback; + _player.allowsExternalPlayback = _allowsExternalPlayback; } - (void)setPlayWhenInactive:(BOOL)playWhenInactive @@ -657,7 +665,7 @@ static int const RCTVideoUnset = -1; [_player play]; [_player setRate:_rate]; } - + _paused = paused; } @@ -745,7 +753,7 @@ static int const RCTVideoUnset = -1; [_player setVolume:_volume]; [_player setMuted:NO]; } - + [self setSelectedAudioTrack:_selectedAudioTrack]; [self setSelectedTextTrack:_selectedTextTrack]; [self setResizeMode:_resizeMode]; @@ -762,52 +770,52 @@ static int const RCTVideoUnset = -1; - (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)characteristic withCriteria:(NSDictionary *)criteria { - NSString *type = criteria[@"type"]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:characteristic]; - AVMediaSelectionOption *mediaOption; - - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { - NSString *value = criteria[@"value"]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *optionValue; - if ([type isEqualToString:@"language"]) { - optionValue = [currentOption extendedLanguageTag]; - } else { - optionValue = [[[currentOption commonMetadata] - valueForKey:@"value"] - objectAtIndex:0]; - } - if ([value isEqualToString:optionValue]) { - mediaOption = currentOption; - break; - } - } - //} else if ([type isEqualToString:@"default"]) { - // option = group.defaultOption; */ - } else if ([type isEqualToString:@"index"]) { - if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [criteria[@"value"] intValue]; - if (group.options.count > index) { - mediaOption = [group.options objectAtIndex:index]; - } - } - } else { // default. invalid type or "system" - [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; - return; + NSString *type = criteria[@"type"]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:characteristic]; + AVMediaSelectionOption *mediaOption; + + if ([type isEqualToString:@"disabled"]) { + // Do nothing. We want to ensure option is nil + } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { + NSString *value = criteria[@"value"]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *optionValue; + if ([type isEqualToString:@"language"]) { + optionValue = [currentOption extendedLanguageTag]; + } else { + optionValue = [[[currentOption commonMetadata] + valueForKey:@"value"] + objectAtIndex:0]; + } + if ([value isEqualToString:optionValue]) { + mediaOption = currentOption; + break; + } } - - // If a match isn't found, option will be nil and text tracks will be disabled - [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; + //} else if ([type isEqualToString:@"default"]) { + // option = group.defaultOption; */ + } else if ([type isEqualToString:@"index"]) { + if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { + int index = [criteria[@"value"] intValue]; + if (group.options.count > index) { + mediaOption = [group.options objectAtIndex:index]; + } + } + } else { // default. invalid type or "system" + [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; + return; + } + + // If a match isn't found, option will be nil and text tracks will be disabled + [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; } - (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack { - _selectedAudioTrack = selectedAudioTrack; - [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible - withCriteria:_selectedAudioTrack]; + _selectedAudioTrack = selectedAudioTrack; + [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible + withCriteria:_selectedAudioTrack]; } - (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack { @@ -879,7 +887,7 @@ static int const RCTVideoUnset = -1; } } } - + for (int i = firstTextIndex; i < _player.currentItem.tracks.count; ++i) { BOOL isEnabled = NO; if (selectedTrackIndex != RCTVideoUnset) { @@ -935,32 +943,32 @@ static int const RCTVideoUnset = -1; - (void)setTextTracks:(NSArray*) textTracks; { _textTracks = textTracks; - + // in case textTracks was set after selectedTextTrack if (_selectedTextTrack) [self setSelectedTextTrack:_selectedTextTrack]; } - (NSArray *)getAudioTrackInfo { - NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *title = @""; - NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; - if (values.count > 0) { - title = [values objectAtIndex:0]; - } - NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; - NSDictionary *audioTrack = @{ - @"index": [NSNumber numberWithInt:i], - @"title": title, - @"language": language - }; - [audioTracks addObject:audioTrack]; + NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *title = @""; + NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; + if (values.count > 0) { + title = [values objectAtIndex:0]; } - return audioTracks; + NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; + NSDictionary *audioTrack = @{ + @"index": [NSNumber numberWithInt:i], + @"title": title, + @"language": language + }; + [audioTracks addObject:audioTrack]; + } + return audioTracks; } - (NSArray *)getTextTrackInfo @@ -992,67 +1000,91 @@ static int const RCTVideoUnset = -1; - (BOOL)getFullscreen { - return _fullscreenPlayerPresented; + return _fullscreenPlayerPresented; } -- (void)setFullscreen:(BOOL)fullscreen -{ - _fullscreen = fullscreen; - if( fullscreen && !_fullscreenPlayerPresented && _player ) - { - // 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)setFullscreen:(BOOL) fullscreen { + _fullscreen = fullscreen; + + NSString* enabled = _fullscreen ? @"1" : @"0"; + + if (!_fullscreenOptions) { + [self setFullscreenOptions: + @{ + @"enabled": enabled, + @"autorotate": @"0", + @"preferredOrientation": @"default" }]; + } + else { + [_fullscreenOptions setValue:enabled forKey:@"enabled"]; + [self setFullscreenOptions:_fullscreenOptions]; + } +} + +- (void)setFullscreenOptions:(NSDictionary*) fullscreenOptions +{ + _fullscreenOptions = fullscreenOptions; + + if (!_fullscreenOptions) return; + + BOOL fullscreenEnabled = [RCTConvert BOOL:[_fullscreenOptions objectForKey:@"enabled"]]; + + if( fullscreenEnabled && !_fullscreenPlayerPresented && _player ) + { + // 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 = fullscreenEnabled; + if(self.onVideoFullscreenPlayerDidPresent) { + self.onVideoFullscreenPlayerDidPresent(@{@"target": self.reactTag}); + } + }]; + } + } + else if ( !fullscreenEnabled && _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 @@ -1076,27 +1108,27 @@ static int const RCTVideoUnset = -1; - (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 { _progressUpdateInterval = progressUpdateInterval; - + if (_timeObserver) { [self removePlayerTimeObserver]; [self addPlayerTimeObserver]; @@ -1117,24 +1149,24 @@ static int const RCTVideoUnset = -1; - (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 @@ -1147,15 +1179,15 @@ static int const RCTVideoUnset = -1; { [self setControls:true]; } - + 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; } @@ -1164,7 +1196,7 @@ static int const RCTVideoUnset = -1; { if( _controls ) { - [subview removeFromSuperview]; + [subview removeFromSuperview]; } else { @@ -1179,7 +1211,7 @@ static int const RCTVideoUnset = -1; if( _controls ) { _playerViewController.view.frame = self.bounds; - + // also adjust all subviews of contentOverlayView for (UIView* subview in _playerViewController.contentOverlayView.subviews) { subview.frame = self.bounds; @@ -1187,10 +1219,10 @@ static int const RCTVideoUnset = -1; } else { - [CATransaction begin]; - [CATransaction setAnimationDuration:0]; - _playerLayer.frame = self.bounds; - [CATransaction commit]; + [CATransaction begin]; + [CATransaction setAnimationDuration:0]; + _playerLayer.frame = self.bounds; + [CATransaction commit]; } } @@ -1204,18 +1236,18 @@ static int const RCTVideoUnset = -1; _playbackRateObserverRegistered = NO; } _player = nil; - + [self removePlayerLayer]; - + [_playerViewController.view removeFromSuperview]; _playerViewController = nil; - + [self removePlayerTimeObserver]; [self removePlayerItemObservers]; - + _eventDispatcher = nil; [[NSNotificationCenter defaultCenter] removeObserver:self]; - + [super removeFromSuperview]; } diff --git a/ios/RCTVideoManager.m b/ios/RCTVideoManager.m index e0e0162e..9d902392 100644 --- a/ios/RCTVideoManager.m +++ b/ios/RCTVideoManager.m @@ -36,7 +36,8 @@ RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString); RCT_EXPORT_VIEW_PROPERTY(rate, float); RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(currentTime, float); -RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL); +RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL); // deprecated property +RCT_EXPORT_VIEW_PROPERTY(fullscreenOptions, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float); /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */ RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock); diff --git a/ios/RCTVideoPlayerViewController.h b/ios/RCTVideoPlayerViewController.h index d427b63d..ed9ebdde 100644 --- a/ios/RCTVideoPlayerViewController.h +++ b/ios/RCTVideoPlayerViewController.h @@ -12,4 +12,9 @@ @interface RCTVideoPlayerViewController : AVPlayerViewController @property (nonatomic, weak) id rctDelegate; + +// Optional paramters +@property (nonatomic, weak) NSString* preferredOrientation; +@property (nonatomic) BOOL autorotate; + @end diff --git a/ios/RCTVideoPlayerViewController.m b/ios/RCTVideoPlayerViewController.m index 12480d54..b79a2de2 100644 --- a/ios/RCTVideoPlayerViewController.m +++ b/ios/RCTVideoPlayerViewController.m @@ -6,6 +6,14 @@ @implementation RCTVideoPlayerViewController +- (id)init { + self = [super init]; + if (self) { + self.autorotate = false; + } + return self; +} + - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; @@ -14,17 +22,25 @@ } - (BOOL)shouldAutorotate { - return YES; + return self.autorotate; } - (UIInterfaceOrientationMask)supportedInterfaceOrientations { - return UIInterfaceOrientationMaskLandscape; + return UIInterfaceOrientationMaskAll; } - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { - - return UIInterfaceOrientationLandscapeLeft; + if ([self.preferredOrientation.lowercaseString isEqualToString:@"landscape"]) { + return UIInterfaceOrientationLandscapeLeft; + } + else if ([self.preferredOrientation.lowercaseString isEqualToString:@"portrait"]) { + return UIInterfaceOrientationPortrait; + } + else { // default case + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + return orientation; + } } @end From 887010f291266d9e8f53ec3a525ee4a602d6ed27 Mon Sep 17 00:00:00 2001 From: Ash Mishra Date: Tue, 14 Aug 2018 10:29:01 -0700 Subject: [PATCH 04/43] merge RCTVideo.m --- ios/RCTVideo.m | 352 ++++++++++++++++++++++++------------------------- 1 file changed, 176 insertions(+), 176 deletions(-) diff --git a/ios/RCTVideo.m b/ios/RCTVideo.m index 3adab702..c78fcf74 100644 --- a/ios/RCTVideo.m +++ b/ios/RCTVideo.m @@ -129,24 +129,24 @@ static int const RCTVideoUnset = -1; - (CMTime)playerItemDuration { - AVPlayerItem *playerItem = [_player currentItem]; - if (playerItem.status == AVPlayerItemStatusReadyToPlay) - { - return([playerItem duration]); - } - - return(kCMTimeInvalid); + 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); + AVPlayerItem *playerItem = [_player currentItem]; + if (playerItem.status == AVPlayerItemStatusReadyToPlay) + { + return [playerItem seekableTimeRanges].firstObject.CMTimeRangeValue; + } + + return (kCMTimeRangeZero); } -(void)addPlayerTimeObserver @@ -164,11 +164,11 @@ static int const RCTVideoUnset = -1; /* 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 @@ -211,43 +211,43 @@ static int const RCTVideoUnset = -1; - (void)audioRouteChanged:(NSNotification *)notification { - NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; - NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; - if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { - self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); - } + NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; + NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; + if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { + self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); + } } #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); - - [[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], - }); - } + 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); + + [[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], + }); + } } /*! @@ -277,12 +277,12 @@ static int const RCTVideoUnset = -1; - (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 @@ -346,13 +346,13 @@ static int const RCTVideoUnset = -1; 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 + }); } - + }); _videoLoadStarted = YES; } @@ -391,11 +391,11 @@ static int const RCTVideoUnset = -1; if (isNetwork) { /* Per #1091, this is not a public API. We need to either get approval from Apple to use this * or use a different approach. - NSDictionary *headers = [source objectForKey:@"requestHeaders"]; - if ([headers count] > 0) { - [assetOptions setObject:headers forKey:@"AVURLAssetHTTPHeaderFieldsKey"]; - } - */ + NSDictionary *headers = [source objectForKey:@"requestHeaders"]; + if ([headers count] > 0) { + [assetOptions setObject:headers forKey:@"AVURLAssetHTTPHeaderFieldsKey"]; + } + */ NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; [assetOptions setObject:cookies forKey:AVURLAssetHTTPCookiesKey]; asset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:uri] options:assetOptions]; @@ -442,9 +442,9 @@ static int const RCTVideoUnset = -1; addMutableTrackWithMediaType:AVMediaTypeText preferredTrackID:kCMPersistentTrackID_Invalid]; [textCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) - ofTrack:textTrackAsset - atTime:kCMTimeZero - error:nil]; + ofTrack:textTrackAsset + atTime:kCMTimeZero + error:nil]; } if (validTextTracks.count != _textTracks.count) { [self setTextTracks:validTextTracks]; @@ -602,7 +602,7 @@ static int const RCTVideoUnset = -1; - (void)playerItemDidReachEnd:(NSNotification *)notification { if(self.onVideoEnd) { - self.onVideoEnd(@{@"target": self.reactTag}); + self.onVideoEnd(@{@"target": self.reactTag}); } if (_repeat) { @@ -636,8 +636,8 @@ static int const RCTVideoUnset = -1; - (void)setAllowsExternalPlayback:(BOOL)allowsExternalPlayback { - _allowsExternalPlayback = allowsExternalPlayback; - _player.allowsExternalPlayback = _allowsExternalPlayback; + _allowsExternalPlayback = allowsExternalPlayback; + _player.allowsExternalPlayback = _allowsExternalPlayback; } - (void)setPlayWhenInactive:(BOOL)playWhenInactive @@ -770,52 +770,52 @@ static int const RCTVideoUnset = -1; - (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)characteristic withCriteria:(NSDictionary *)criteria { - NSString *type = criteria[@"type"]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:characteristic]; - AVMediaSelectionOption *mediaOption; - - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { - NSString *value = criteria[@"value"]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *optionValue; - if ([type isEqualToString:@"language"]) { - optionValue = [currentOption extendedLanguageTag]; - } else { - optionValue = [[[currentOption commonMetadata] - valueForKey:@"value"] - objectAtIndex:0]; - } - if ([value isEqualToString:optionValue]) { - mediaOption = currentOption; - break; - } + NSString *type = criteria[@"type"]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:characteristic]; + AVMediaSelectionOption *mediaOption; + + if ([type isEqualToString:@"disabled"]) { + // Do nothing. We want to ensure option is nil + } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { + NSString *value = criteria[@"value"]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *optionValue; + if ([type isEqualToString:@"language"]) { + optionValue = [currentOption extendedLanguageTag]; + } else { + optionValue = [[[currentOption commonMetadata] + valueForKey:@"value"] + objectAtIndex:0]; + } + if ([value isEqualToString:optionValue]) { + mediaOption = currentOption; + break; + } + } + //} else if ([type isEqualToString:@"default"]) { + // option = group.defaultOption; */ + } else if ([type isEqualToString:@"index"]) { + if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { + int index = [criteria[@"value"] intValue]; + if (group.options.count > index) { + mediaOption = [group.options objectAtIndex:index]; + } + } + } else { // default. invalid type or "system" + [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; + return; } - //} else if ([type isEqualToString:@"default"]) { - // option = group.defaultOption; */ - } else if ([type isEqualToString:@"index"]) { - if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [criteria[@"value"] intValue]; - if (group.options.count > index) { - mediaOption = [group.options objectAtIndex:index]; - } - } - } else { // default. invalid type or "system" - [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; - return; - } - - // If a match isn't found, option will be nil and text tracks will be disabled - [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; + + // If a match isn't found, option will be nil and text tracks will be disabled + [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; } - (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack { - _selectedAudioTrack = selectedAudioTrack; - [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible - withCriteria:_selectedAudioTrack]; + _selectedAudioTrack = selectedAudioTrack; + [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible + withCriteria:_selectedAudioTrack]; } - (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack { @@ -950,25 +950,25 @@ static int const RCTVideoUnset = -1; - (NSArray *)getAudioTrackInfo { - NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *title = @""; - NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; - if (values.count > 0) { - title = [values objectAtIndex:0]; + NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *title = @""; + NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; + if (values.count > 0) { + title = [values objectAtIndex:0]; + } + NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; + NSDictionary *audioTrack = @{ + @"index": [NSNumber numberWithInt:i], + @"title": title, + @"language": language + }; + [audioTracks addObject:audioTrack]; } - NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; - NSDictionary *audioTrack = @{ - @"index": [NSNumber numberWithInt:i], - @"title": title, - @"language": language - }; - [audioTracks addObject:audioTrack]; - } - return audioTracks; + return audioTracks; } - (NSArray *)getTextTrackInfo @@ -1000,7 +1000,7 @@ static int const RCTVideoUnset = -1; - (BOOL)getFullscreen { - return _fullscreenPlayerPresented; + return _fullscreenPlayerPresented; } - (void)setFullscreen:(BOOL) fullscreen { @@ -1077,14 +1077,14 @@ static int const RCTVideoUnset = -1; - (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 @@ -1108,21 +1108,21 @@ static int const RCTVideoUnset = -1; - (void)setControls:(BOOL)controls { - if( _controls != controls || (!_playerLayer && !_playerViewController) ) - { - _controls = controls; - if( _controls ) + if( _controls != controls || (!_playerLayer && !_playerViewController) ) { - [self removePlayerLayer]; - [self usePlayerViewController]; + _controls = controls; + if( _controls ) + { + [self removePlayerLayer]; + [self usePlayerViewController]; + } + else + { + [_playerViewController.view removeFromSuperview]; + _playerViewController = nil; + [self usePlayerLayer]; + } } - else - { - [_playerViewController.view removeFromSuperview]; - _playerViewController = nil; - [self usePlayerLayer]; - } - } } - (void)setProgressUpdateInterval:(float)progressUpdateInterval @@ -1149,24 +1149,24 @@ static int const RCTVideoUnset = -1; - (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 @@ -1182,12 +1182,12 @@ static int const RCTVideoUnset = -1; 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; } @@ -1196,7 +1196,7 @@ static int const RCTVideoUnset = -1; { if( _controls ) { - [subview removeFromSuperview]; + [subview removeFromSuperview]; } else { @@ -1219,10 +1219,10 @@ static int const RCTVideoUnset = -1; } else { - [CATransaction begin]; - [CATransaction setAnimationDuration:0]; - _playerLayer.frame = self.bounds; - [CATransaction commit]; + [CATransaction begin]; + [CATransaction setAnimationDuration:0]; + _playerLayer.frame = self.bounds; + [CATransaction commit]; } } From f7fd5dc5b70c5394cd1ab826c29083d26f9555eb Mon Sep 17 00:00:00 2001 From: Ash Mishra Date: Thu, 16 Aug 2018 15:32:36 -0700 Subject: [PATCH 05/43] Fix for files that are stored in the Documents folder on iOS (and not as resources in the JS app) --- ios/RCTVideo.m | 364 ++++++++++++++++++++++++------------------------- 1 file changed, 182 insertions(+), 182 deletions(-) diff --git a/ios/RCTVideo.m b/ios/RCTVideo.m index c78fcf74..455856c5 100644 --- a/ios/RCTVideo.m +++ b/ios/RCTVideo.m @@ -129,24 +129,24 @@ static int const RCTVideoUnset = -1; - (CMTime)playerItemDuration { - AVPlayerItem *playerItem = [_player currentItem]; - if (playerItem.status == AVPlayerItemStatusReadyToPlay) - { - return([playerItem duration]); - } - - return(kCMTimeInvalid); + 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); + AVPlayerItem *playerItem = [_player currentItem]; + if (playerItem.status == AVPlayerItemStatusReadyToPlay) + { + return [playerItem seekableTimeRanges].firstObject.CMTimeRangeValue; + } + + return (kCMTimeRangeZero); } -(void)addPlayerTimeObserver @@ -164,11 +164,11 @@ static int const RCTVideoUnset = -1; /* 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 @@ -211,43 +211,43 @@ static int const RCTVideoUnset = -1; - (void)audioRouteChanged:(NSNotification *)notification { - NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; - NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; - if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { - self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); - } + NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; + NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; + if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { + self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); + } } #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); - - [[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], - }); - } + 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); + + [[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], + }); + } } /*! @@ -277,12 +277,12 @@ static int const RCTVideoUnset = -1; - (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 @@ -346,19 +346,19 @@ static int const RCTVideoUnset = -1; 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 + }); } - + }); _videoLoadStarted = YES; } - (NSURL*) urlFilePath:(NSString*) filepath { - if ([filepath containsString:@"file://"]) { + if ([filepath containsString:@"file://"] && ![filepath containsString:@"/Documents/"]) { return [NSURL URLWithString:filepath]; } @@ -391,11 +391,11 @@ static int const RCTVideoUnset = -1; if (isNetwork) { /* Per #1091, this is not a public API. We need to either get approval from Apple to use this * or use a different approach. - NSDictionary *headers = [source objectForKey:@"requestHeaders"]; - if ([headers count] > 0) { - [assetOptions setObject:headers forKey:@"AVURLAssetHTTPHeaderFieldsKey"]; - } - */ + NSDictionary *headers = [source objectForKey:@"requestHeaders"]; + if ([headers count] > 0) { + [assetOptions setObject:headers forKey:@"AVURLAssetHTTPHeaderFieldsKey"]; + } + */ NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; [assetOptions setObject:cookies forKey:AVURLAssetHTTPCookiesKey]; asset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:uri] options:assetOptions]; @@ -442,9 +442,9 @@ static int const RCTVideoUnset = -1; addMutableTrackWithMediaType:AVMediaTypeText preferredTrackID:kCMPersistentTrackID_Invalid]; [textCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) - ofTrack:textTrackAsset - atTime:kCMTimeZero - error:nil]; + ofTrack:textTrackAsset + atTime:kCMTimeZero + error:nil]; } if (validTextTracks.count != _textTracks.count) { [self setTextTracks:validTextTracks]; @@ -602,7 +602,7 @@ static int const RCTVideoUnset = -1; - (void)playerItemDidReachEnd:(NSNotification *)notification { if(self.onVideoEnd) { - self.onVideoEnd(@{@"target": self.reactTag}); + self.onVideoEnd(@{@"target": self.reactTag}); } if (_repeat) { @@ -636,8 +636,8 @@ static int const RCTVideoUnset = -1; - (void)setAllowsExternalPlayback:(BOOL)allowsExternalPlayback { - _allowsExternalPlayback = allowsExternalPlayback; - _player.allowsExternalPlayback = _allowsExternalPlayback; + _allowsExternalPlayback = allowsExternalPlayback; + _player.allowsExternalPlayback = _allowsExternalPlayback; } - (void)setPlayWhenInactive:(BOOL)playWhenInactive @@ -770,52 +770,52 @@ static int const RCTVideoUnset = -1; - (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)characteristic withCriteria:(NSDictionary *)criteria { - NSString *type = criteria[@"type"]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:characteristic]; - AVMediaSelectionOption *mediaOption; - - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { - NSString *value = criteria[@"value"]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *optionValue; - if ([type isEqualToString:@"language"]) { - optionValue = [currentOption extendedLanguageTag]; - } else { - optionValue = [[[currentOption commonMetadata] - valueForKey:@"value"] - objectAtIndex:0]; - } - if ([value isEqualToString:optionValue]) { - mediaOption = currentOption; - break; - } - } - //} else if ([type isEqualToString:@"default"]) { - // option = group.defaultOption; */ - } else if ([type isEqualToString:@"index"]) { - if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [criteria[@"value"] intValue]; - if (group.options.count > index) { - mediaOption = [group.options objectAtIndex:index]; - } - } - } else { // default. invalid type or "system" - [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; - return; + NSString *type = criteria[@"type"]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:characteristic]; + AVMediaSelectionOption *mediaOption; + + if ([type isEqualToString:@"disabled"]) { + // Do nothing. We want to ensure option is nil + } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { + NSString *value = criteria[@"value"]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *optionValue; + if ([type isEqualToString:@"language"]) { + optionValue = [currentOption extendedLanguageTag]; + } else { + optionValue = [[[currentOption commonMetadata] + valueForKey:@"value"] + objectAtIndex:0]; + } + if ([value isEqualToString:optionValue]) { + mediaOption = currentOption; + break; + } } - - // If a match isn't found, option will be nil and text tracks will be disabled - [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; + //} else if ([type isEqualToString:@"default"]) { + // option = group.defaultOption; */ + } else if ([type isEqualToString:@"index"]) { + if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { + int index = [criteria[@"value"] intValue]; + if (group.options.count > index) { + mediaOption = [group.options objectAtIndex:index]; + } + } + } else { // default. invalid type or "system" + [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; + return; + } + + // If a match isn't found, option will be nil and text tracks will be disabled + [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; } - (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack { - _selectedAudioTrack = selectedAudioTrack; - [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible - withCriteria:_selectedAudioTrack]; + _selectedAudioTrack = selectedAudioTrack; + [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible + withCriteria:_selectedAudioTrack]; } - (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack { @@ -950,25 +950,25 @@ static int const RCTVideoUnset = -1; - (NSArray *)getAudioTrackInfo { - NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *title = @""; - NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; - if (values.count > 0) { - title = [values objectAtIndex:0]; - } - NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; - NSDictionary *audioTrack = @{ - @"index": [NSNumber numberWithInt:i], - @"title": title, - @"language": language - }; - [audioTracks addObject:audioTrack]; + NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *title = @""; + NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; + if (values.count > 0) { + title = [values objectAtIndex:0]; } - return audioTracks; + NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; + NSDictionary *audioTrack = @{ + @"index": [NSNumber numberWithInt:i], + @"title": title, + @"language": language + }; + [audioTracks addObject:audioTrack]; + } + return audioTracks; } - (NSArray *)getTextTrackInfo @@ -1000,7 +1000,7 @@ static int const RCTVideoUnset = -1; - (BOOL)getFullscreen { - return _fullscreenPlayerPresented; + return _fullscreenPlayerPresented; } - (void)setFullscreen:(BOOL) fullscreen { @@ -1010,11 +1010,11 @@ static int const RCTVideoUnset = -1; if (!_fullscreenOptions) { [self setFullscreenOptions: - @{ - @"enabled": enabled, - @"autorotate": @"0", - @"preferredOrientation": @"default" - }]; + @{ + @"enabled": enabled, + @"autorotate": @"0", + @"preferredOrientation": @"default" + }]; } else { [_fullscreenOptions setValue:enabled forKey:@"enabled"]; @@ -1077,14 +1077,14 @@ static int const RCTVideoUnset = -1; - (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 @@ -1108,21 +1108,21 @@ static int const RCTVideoUnset = -1; - (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 @@ -1149,24 +1149,24 @@ static int const RCTVideoUnset = -1; - (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 @@ -1182,12 +1182,12 @@ static int const RCTVideoUnset = -1; 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; } @@ -1196,7 +1196,7 @@ static int const RCTVideoUnset = -1; { if( _controls ) { - [subview removeFromSuperview]; + [subview removeFromSuperview]; } else { @@ -1219,10 +1219,10 @@ static int const RCTVideoUnset = -1; } else { - [CATransaction begin]; - [CATransaction setAnimationDuration:0]; - _playerLayer.frame = self.bounds; - [CATransaction commit]; + [CATransaction begin]; + [CATransaction setAnimationDuration:0]; + _playerLayer.frame = self.bounds; + [CATransaction commit]; } } From 7d805f13633f5a93d343ba6930ee99994423f57a Mon Sep 17 00:00:00 2001 From: Ash Mishra Date: Mon, 20 Aug 2018 11:52:06 -0700 Subject: [PATCH 06/43] Improvements to urlFilePath so that it only does a /Documents/ folder check if the file does not exist; improvements to full-screen options for iOS to default autorotate, and to respect landscape / portrait masks --- ios/RCTVideo.m | 10 ++++++---- ios/RCTVideoPlayerViewController.m | 10 ++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ios/RCTVideo.m b/ios/RCTVideo.m index 455856c5..8e810b6b 100644 --- a/ios/RCTVideo.m +++ b/ios/RCTVideo.m @@ -358,15 +358,17 @@ static int const RCTVideoUnset = -1; } - (NSURL*) urlFilePath:(NSString*) filepath { - if ([filepath containsString:@"file://"] && ![filepath containsString:@"/Documents/"]) { - return [NSURL URLWithString:filepath]; + + // check if the file exists at the specified location + if ([[NSFileManager defaultManager] fileExistsAtPath:filepath]) { + return [NSURL fileURLWithPath:filepath]; } - // code to support local caching + // if no file found, check if the file exists in the Document directory NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString* relativeFilePath = [filepath lastPathComponent]; // the file may be multiple levels below the documents directory - NSArray* fileComponents = [filepath componentsSeparatedByString:@"Documents/"]; + NSArray* fileComponents = [filepath componentsSeparatedByString:@"/Documents/"]; if (fileComponents.count>1) { relativeFilePath = [fileComponents objectAtIndex:1]; } diff --git a/ios/RCTVideoPlayerViewController.m b/ios/RCTVideoPlayerViewController.m index b79a2de2..fe8cb171 100644 --- a/ios/RCTVideoPlayerViewController.m +++ b/ios/RCTVideoPlayerViewController.m @@ -9,7 +9,7 @@ - (id)init { self = [super init]; if (self) { - self.autorotate = false; + self.autorotate = true; // autorotate should be true by default } return self; } @@ -27,12 +27,18 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations { + if ([self.preferredOrientation.lowercaseString isEqualToString:@"landscape"]) { + return UIInterfaceOrientationMaskLandscape; + } + else if ([self.preferredOrientation.lowercaseString isEqualToString:@"portrait"]) { + return UIInterfaceOrientationMaskPortrait; + } return UIInterfaceOrientationMaskAll; } - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { if ([self.preferredOrientation.lowercaseString isEqualToString:@"landscape"]) { - return UIInterfaceOrientationLandscapeLeft; + return UIInterfaceOrientationLandscapeRight; } else if ([self.preferredOrientation.lowercaseString isEqualToString:@"portrait"]) { return UIInterfaceOrientationPortrait; From 7b8f79b36a6eda1452b3cab1f35165f2ebcf9769 Mon Sep 17 00:00:00 2001 From: Ash Mishra Date: Mon, 27 Aug 2018 10:42:49 -0700 Subject: [PATCH 07/43] added an onCaptionsDeviceSettings event --- Video.js | 8 ++++++++ ios/RCTVideo.h | 1 + ios/RCTVideo.m | 15 ++++++++++++--- ios/RCTVideoManager.m | 1 + 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Video.js b/Video.js index bf64e9d5..58f57922 100644 --- a/Video.js +++ b/Video.js @@ -151,6 +151,12 @@ export default class Video extends Component { } }; + _onCaptionsDeviceSetting = (event) => { + if (this.props.onCaptionsDeviceSetting) { + this.props.onCaptionsDeviceSetting(event.nativeEvent); + } + }; + _onPlaybackStalled = (event) => { if (this.props.onPlaybackStalled) { this.props.onPlaybackStalled(event.nativeEvent); @@ -241,6 +247,7 @@ export default class Video extends Component { onVideoFullscreenPlayerWillDismiss: this._onFullscreenPlayerWillDismiss, onVideoFullscreenPlayerDidDismiss: this._onFullscreenPlayerDidDismiss, onReadyForDisplay: this._onReadyForDisplay, + onCaptionsDeviceSetting: this._onCaptionsDeviceSetting, onPlaybackStalled: this._onPlaybackStalled, onPlaybackResume: this._onPlaybackResume, onPlaybackRateChange: this._onPlaybackRateChange, @@ -379,6 +386,7 @@ Video.propTypes = { onFullscreenPlayerWillDismiss: PropTypes.func, onFullscreenPlayerDidDismiss: PropTypes.func, onReadyForDisplay: PropTypes.func, + onCaptionsDeviceSetting: PropTypes.func, onPlaybackStalled: PropTypes.func, onPlaybackResume: PropTypes.func, onPlaybackRateChange: PropTypes.func, diff --git a/ios/RCTVideo.h b/ios/RCTVideo.h index 1c471236..a556b3c3 100644 --- a/ios/RCTVideo.h +++ b/ios/RCTVideo.h @@ -23,6 +23,7 @@ @property (nonatomic, copy) RCTBubblingEventBlock onVideoFullscreenPlayerWillDismiss; @property (nonatomic, copy) RCTBubblingEventBlock onVideoFullscreenPlayerDidDismiss; @property (nonatomic, copy) RCTBubblingEventBlock onReadyForDisplay; +@property (nonatomic, copy) RCTBubblingEventBlock onCaptionsDeviceSetting; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackStalled; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackResume; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackRateChange; diff --git a/ios/RCTVideo.m b/ios/RCTVideo.m index 8e810b6b..f5e7f4e8 100644 --- a/ios/RCTVideo.m +++ b/ios/RCTVideo.m @@ -59,6 +59,7 @@ static int const RCTVideoUnset = -1; NSDictionary* _fullscreenOptions; BOOL _fullscreenPlayerPresented; UIViewController * _presentingViewController; + BOOL _deviceCaptionsEnabled; } - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher @@ -316,6 +317,10 @@ static int const RCTVideoUnset = -1; [self removePlayerTimeObserver]; [self removePlayerItemObservers]; + CFArrayRef captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser); + NSArray *captionSettings = (__bridge NSArray*)captioningMediaCharacteristics; + _deviceCaptionsEnabled = [captionSettings containsObject:AVMediaCharacteristicTranscribesSpokenDialogForAccessibility]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // perform on next run loop, otherwise other passed react-props may not be set @@ -353,6 +358,12 @@ static int const RCTVideoUnset = -1; }); } + if (self.onCaptionsDeviceSetting) { + self.onCaptionsDeviceSetting(@{@"deviceCaptionsEnabled": [NSNumber numberWithBool:_deviceCaptionsEnabled], + @"target": self.reactTag + }); + } + }); _videoLoadStarted = YES; } @@ -875,9 +886,7 @@ static int const RCTVideoUnset = -1; // in the situation that a selected text track is not available (eg. specifies a textTrack not available) if (![type isEqualToString:@"disabled"] && selectedTrackIndex == RCTVideoUnset) { - CFArrayRef captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser); - NSArray *captionSettings = (__bridge NSArray*)captioningMediaCharacteristics; - if ([captionSettings containsObject:AVMediaCharacteristicTranscribesSpokenDialogForAccessibility]) { + if (_deviceCaptionsEnabled) { selectedTrackIndex = 0; // If we can't find a match, use the first available track NSString *systemLanguage = [[NSLocale preferredLanguages] firstObject]; for (int i = 0; i < textTracks.count; ++i) { diff --git a/ios/RCTVideoManager.m b/ios/RCTVideoManager.m index 9d902392..683238a1 100644 --- a/ios/RCTVideoManager.m +++ b/ios/RCTVideoManager.m @@ -54,6 +54,7 @@ RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerDidPresent, RCTBubblingEventBloc RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerWillDismiss, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerDidDismiss, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onReadyForDisplay, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onCaptionsDeviceSetting, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackStalled, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTBubblingEventBlock); From 3ba26eb45a7b6602c5051e2a20e272afcad25852 Mon Sep 17 00:00:00 2001 From: Ash Mishra Date: Thu, 2 Aug 2018 10:32:50 -0700 Subject: [PATCH 08/43] Working on autorotation of video player --- ios/Video/RCTVideo.m | 9 +++++---- ios/Video/RCTVideoPlayerViewController.m | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 9fe30b63..3b04e4e5 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -61,6 +61,7 @@ static int const RCTVideoUnset = -1; BOOL _playWhenInactive; NSString * _ignoreSilentSwitch; NSString * _resizeMode; + BOOL _fullscreen; BOOL _fullscreenPlayerPresented; UIViewController * _presentingViewController; #if __has_include() @@ -341,6 +342,8 @@ static int const RCTVideoUnset = -1; _playbackRateObserverRegistered = YES; [self addPlayerTimeObserver]; + + [self setFullscreen:_fullscreen]; //Perform on next run loop, otherwise onVideoLoadStart is nil if (self.onVideoLoadStart) { @@ -1085,10 +1088,8 @@ static int const RCTVideoUnset = -1; - (void)setFullscreen:(BOOL)fullscreen { - if( fullscreen && !_fullscreenPlayerPresented ) - { - // Ensure player view controller is not null - if( !_playerViewController ) + _fullscreen = fullscreen; + if( fullscreen && !_fullscreenPlayerPresented && _player ) { [self usePlayerViewController]; } diff --git a/ios/Video/RCTVideoPlayerViewController.m b/ios/Video/RCTVideoPlayerViewController.m index 7809221a..12480d54 100644 --- a/ios/Video/RCTVideoPlayerViewController.m +++ b/ios/Video/RCTVideoPlayerViewController.m @@ -13,4 +13,18 @@ [_rctDelegate videoPlayerViewControllerDidDismiss:self]; } +- (BOOL)shouldAutorotate { + return YES; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + + return UIInterfaceOrientationMaskLandscape; +} + +- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { + + return UIInterfaceOrientationLandscapeLeft; +} + @end From f45d6a2c3e63e9904f9472325ba2dcd960504a88 Mon Sep 17 00:00:00 2001 From: Ash Mishra Date: Thu, 9 Aug 2018 09:58:03 -0700 Subject: [PATCH 09/43] Added fullscreen options for iOS Player --- Video.js | 5 + ios/Video/RCTVideo.m | 219 +++++++++++++---------- ios/Video/RCTVideoManager.m | 3 +- ios/Video/RCTVideoPlayerViewController.h | 5 + ios/Video/RCTVideoPlayerViewController.m | 24 ++- 5 files changed, 159 insertions(+), 97 deletions(-) diff --git a/Video.js b/Video.js index e8203b8b..58a7b9c1 100644 --- a/Video.js +++ b/Video.js @@ -289,6 +289,11 @@ Video.propTypes = { PropTypes.object ]), fullscreen: PropTypes.bool, + fullscreenOptions: PropTypes.shape({ + enabled: PropTypes.bool, + preferredOrientation: PropTypes.string, + autorotate: PropTypes.bool + }), onVideoLoadStart: PropTypes.func, onVideoLoad: PropTypes.func, onVideoBuffer: PropTypes.func, diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 3b04e4e5..a4f11492 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -36,7 +36,7 @@ static int const RCTVideoUnset = -1; RCTEventDispatcher *_eventDispatcher; BOOL _playbackRateObserverRegistered; BOOL _videoLoadStarted; - + bool _pendingSeek; float _pendingSeekTime; float _lastSeekTime; @@ -62,6 +62,7 @@ static int const RCTVideoUnset = -1; NSString * _ignoreSilentSwitch; NSString * _resizeMode; BOOL _fullscreen; + NSDictionary* _fullscreenOptions; BOOL _fullscreenPlayerPresented; UIViewController * _presentingViewController; #if __has_include() @@ -106,7 +107,7 @@ static int const RCTVideoUnset = -1; selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChanged:) name:AVAudioSessionRouteChangeNotification @@ -117,13 +118,20 @@ static int const RCTVideoUnset = -1; } - (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem { - RCTVideoPlayerViewController* playerLayer= [[RCTVideoPlayerViewController alloc] init]; - playerLayer.showsPlaybackControls = YES; - 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 = YES; + playerLayer.rctDelegate = self; + + if (_fullscreenOptions) { + playerLayer.preferredOrientation = [RCTConvert NSString:[_fullscreenOptions objectForKey:@"preferredOrientation"]]; + playerLayer.autorotate = [RCTConvert BOOL:[_fullscreenOptions objectForKey:@"autorotate"]]; + } + + playerLayer.view.frame = self.bounds; + playerLayer.player = player; + playerLayer.view.frame = self.bounds; + return playerLayer; } /* --------------------------------------------------------- @@ -214,11 +222,11 @@ static int const RCTVideoUnset = -1; - (void)audioRouteChanged:(NSNotification *)notification { - NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; - NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; - if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { - self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); - } + NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; + NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; + if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { + self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); + } } #pragma mark - Progress @@ -343,7 +351,7 @@ static int const RCTVideoUnset = -1; [self addPlayerTimeObserver]; - [self setFullscreen:_fullscreen]; + [self setFullscreenOptions:_fullscreenOptions]; //Perform on next run loop, otherwise onVideoLoadStart is nil if (self.onVideoLoadStart) { @@ -398,14 +406,14 @@ static int const RCTVideoUnset = -1; ofTrack:videoAsset atTime:kCMTimeZero error:nil]; - + AVAssetTrack *audioAsset = [asset tracksWithMediaType:AVMediaTypeAudio].firstObject; AVMutableCompositionTrack *audioCompTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; [audioCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) ofTrack:audioAsset atTime:kCMTimeZero error:nil]; - + NSMutableArray* validTextTracks = [NSMutableArray array]; for (int i = 0; i < _textTracks.count; ++i) { AVURLAsset *textURLAsset; @@ -422,9 +430,9 @@ static int const RCTVideoUnset = -1; addMutableTrackWithMediaType:AVMediaTypeText preferredTrackID:kCMPersistentTrackID_Invalid]; [textCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) - ofTrack:textTrackAsset - atTime:kCMTimeZero - error:nil]; + ofTrack:textTrackAsset + atTime:kCMTimeZero + error:nil]; } if (validTextTracks.count != _textTracks.count) { [self setTextTracks:validTextTracks]; @@ -664,7 +672,7 @@ static int const RCTVideoUnset = -1; selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:[_player currentItem]]; - + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemPlaybackStalledNotification object:nil]; @@ -719,8 +727,8 @@ static int const RCTVideoUnset = -1; - (void)setAllowsExternalPlayback:(BOOL)allowsExternalPlayback { - _allowsExternalPlayback = allowsExternalPlayback; - _player.allowsExternalPlayback = _allowsExternalPlayback; + _allowsExternalPlayback = allowsExternalPlayback; + _player.allowsExternalPlayback = _allowsExternalPlayback; } - (void)setPlayWhenInactive:(BOOL)playWhenInactive @@ -836,7 +844,7 @@ static int const RCTVideoUnset = -1; [_player setVolume:_volume]; [_player setMuted:NO]; } - + [self setSelectedAudioTrack:_selectedAudioTrack]; [self setSelectedTextTrack:_selectedTextTrack]; [self setResizeMode:_resizeMode]; @@ -853,52 +861,52 @@ static int const RCTVideoUnset = -1; - (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)characteristic withCriteria:(NSDictionary *)criteria { - NSString *type = criteria[@"type"]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:characteristic]; - AVMediaSelectionOption *mediaOption; - - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { - NSString *value = criteria[@"value"]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *optionValue; - if ([type isEqualToString:@"language"]) { - optionValue = [currentOption extendedLanguageTag]; - } else { - optionValue = [[[currentOption commonMetadata] - valueForKey:@"value"] - objectAtIndex:0]; - } - if ([value isEqualToString:optionValue]) { - mediaOption = currentOption; - break; - } - } - //} else if ([type isEqualToString:@"default"]) { - // option = group.defaultOption; */ - } else if ([type isEqualToString:@"index"]) { - if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [criteria[@"value"] intValue]; - if (group.options.count > index) { - mediaOption = [group.options objectAtIndex:index]; - } - } - } else { // default. invalid type or "system" - [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; - return; + NSString *type = criteria[@"type"]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:characteristic]; + AVMediaSelectionOption *mediaOption; + + if ([type isEqualToString:@"disabled"]) { + // Do nothing. We want to ensure option is nil + } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { + NSString *value = criteria[@"value"]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *optionValue; + if ([type isEqualToString:@"language"]) { + optionValue = [currentOption extendedLanguageTag]; + } else { + optionValue = [[[currentOption commonMetadata] + valueForKey:@"value"] + objectAtIndex:0]; + } + if ([value isEqualToString:optionValue]) { + mediaOption = currentOption; + break; + } } - - // If a match isn't found, option will be nil and text tracks will be disabled - [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; + //} else if ([type isEqualToString:@"default"]) { + // option = group.defaultOption; */ + } else if ([type isEqualToString:@"index"]) { + if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { + int index = [criteria[@"value"] intValue]; + if (group.options.count > index) { + mediaOption = [group.options objectAtIndex:index]; + } + } + } else { // default. invalid type or "system" + [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; + return; + } + + // If a match isn't found, option will be nil and text tracks will be disabled + [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; } - (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack { - _selectedAudioTrack = selectedAudioTrack; - [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible - withCriteria:_selectedAudioTrack]; + _selectedAudioTrack = selectedAudioTrack; + [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible + withCriteria:_selectedAudioTrack]; } - (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack { @@ -970,7 +978,7 @@ static int const RCTVideoUnset = -1; } } } - + for (int i = firstTextIndex; i < _player.currentItem.tracks.count; ++i) { BOOL isEnabled = NO; if (selectedTrackIndex != RCTVideoUnset) { @@ -1026,32 +1034,32 @@ static int const RCTVideoUnset = -1; - (void)setTextTracks:(NSArray*) textTracks; { _textTracks = textTracks; - + // in case textTracks was set after selectedTextTrack if (_selectedTextTrack) [self setSelectedTextTrack:_selectedTextTrack]; } - (NSArray *)getAudioTrackInfo { - NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *title = @""; - NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; - if (values.count > 0) { - title = [values objectAtIndex:0]; - } - NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; - NSDictionary *audioTrack = @{ - @"index": [NSNumber numberWithInt:i], - @"title": title, - @"language": language - }; - [audioTracks addObject:audioTrack]; + NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *title = @""; + NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; + if (values.count > 0) { + title = [values objectAtIndex:0]; } - return audioTracks; + NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; + NSDictionary *audioTrack = @{ + @"index": [NSNumber numberWithInt:i], + @"title": title, + @"language": language + }; + [audioTracks addObject:audioTrack]; + } + return audioTracks; } - (NSArray *)getTextTrackInfo @@ -1086,10 +1094,37 @@ static int const RCTVideoUnset = -1; return _fullscreenPlayerPresented; } -- (void)setFullscreen:(BOOL)fullscreen -{ +- (void)setFullscreen:(BOOL) fullscreen { _fullscreen = fullscreen; - if( fullscreen && !_fullscreenPlayerPresented && _player ) + + NSString* enabled = _fullscreen ? @"1" : @"0"; + + if (!_fullscreenOptions) { + [self setFullscreenOptions: + @{ + @"enabled": enabled, + @"autorotate": @"0", + @"preferredOrientation": @"default" + }]; + } + else { + [_fullscreenOptions setValue:enabled forKey:@"enabled"]; + [self setFullscreenOptions:_fullscreenOptions]; + } +} + +- (void)setFullscreenOptions:(NSDictionary*) fullscreenOptions +{ + _fullscreenOptions = fullscreenOptions; + + if (!_fullscreenOptions) return; + + BOOL fullscreenEnabled = [RCTConvert BOOL:[_fullscreenOptions objectForKey:@"enabled"]]; + + if( fullscreenEnabled && !_fullscreenPlayerPresented && _player ) + { + // Ensure player view controller is not null + if( !_playerViewController ) { [self usePlayerViewController]; } @@ -1115,14 +1150,14 @@ static int const RCTVideoUnset = -1; } [viewController presentViewController:_playerViewController animated:true completion:^{ _playerViewController.showsPlaybackControls = YES; - _fullscreenPlayerPresented = fullscreen; + _fullscreenPlayerPresented = fullscreenEnabled; if(self.onVideoFullscreenPlayerDidPresent) { self.onVideoFullscreenPlayerDidPresent(@{@"target": self.reactTag}); } }]; } } - else if ( !fullscreen && _fullscreenPlayerPresented ) + else if ( !fullscreenEnabled && _fullscreenPlayerPresented ) { [self videoPlayerViewControllerWillDismiss:_playerViewController]; [_presentingViewController dismissViewControllerAnimated:true completion:^{ @@ -1184,7 +1219,7 @@ static int const RCTVideoUnset = -1; - (void)setProgressUpdateInterval:(float)progressUpdateInterval { _progressUpdateInterval = progressUpdateInterval; - + if (_timeObserver) { [self removePlayerTimeObserver]; [self addPlayerTimeObserver]; diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index e0e0162e..9d902392 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -36,7 +36,8 @@ RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString); RCT_EXPORT_VIEW_PROPERTY(rate, float); RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(currentTime, float); -RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL); +RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL); // deprecated property +RCT_EXPORT_VIEW_PROPERTY(fullscreenOptions, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float); /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */ RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock); diff --git a/ios/Video/RCTVideoPlayerViewController.h b/ios/Video/RCTVideoPlayerViewController.h index d427b63d..ed9ebdde 100644 --- a/ios/Video/RCTVideoPlayerViewController.h +++ b/ios/Video/RCTVideoPlayerViewController.h @@ -12,4 +12,9 @@ @interface RCTVideoPlayerViewController : AVPlayerViewController @property (nonatomic, weak) id rctDelegate; + +// Optional paramters +@property (nonatomic, weak) NSString* preferredOrientation; +@property (nonatomic) BOOL autorotate; + @end diff --git a/ios/Video/RCTVideoPlayerViewController.m b/ios/Video/RCTVideoPlayerViewController.m index 12480d54..b79a2de2 100644 --- a/ios/Video/RCTVideoPlayerViewController.m +++ b/ios/Video/RCTVideoPlayerViewController.m @@ -6,6 +6,14 @@ @implementation RCTVideoPlayerViewController +- (id)init { + self = [super init]; + if (self) { + self.autorotate = false; + } + return self; +} + - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; @@ -14,17 +22,25 @@ } - (BOOL)shouldAutorotate { - return YES; + return self.autorotate; } - (UIInterfaceOrientationMask)supportedInterfaceOrientations { - return UIInterfaceOrientationMaskLandscape; + return UIInterfaceOrientationMaskAll; } - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { - - return UIInterfaceOrientationLandscapeLeft; + if ([self.preferredOrientation.lowercaseString isEqualToString:@"landscape"]) { + return UIInterfaceOrientationLandscapeLeft; + } + else if ([self.preferredOrientation.lowercaseString isEqualToString:@"portrait"]) { + return UIInterfaceOrientationPortrait; + } + else { // default case + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + return orientation; + } } @end From 3b0d4592303e41cf4ba0ed69c450970755e91f90 Mon Sep 17 00:00:00 2001 From: Ash Mishra Date: Thu, 16 Aug 2018 15:32:36 -0700 Subject: [PATCH 10/43] Fix for files that are stored in the Documents folder on iOS (and not as resources in the JS app) --- ios/Video/RCTVideo.m | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index a4f11492..8732f768 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -370,7 +370,7 @@ static int const RCTVideoUnset = -1; } - (NSURL*) urlFilePath:(NSString*) filepath { - if ([filepath containsString:@"file://"]) { + if ([filepath containsString:@"file://"] && ![filepath containsString:@"/Documents/"]) { return [NSURL URLWithString:filepath]; } @@ -1101,11 +1101,11 @@ static int const RCTVideoUnset = -1; if (!_fullscreenOptions) { [self setFullscreenOptions: - @{ - @"enabled": enabled, - @"autorotate": @"0", - @"preferredOrientation": @"default" - }]; + @{ + @"enabled": enabled, + @"autorotate": @"0", + @"preferredOrientation": @"default" + }]; } else { [_fullscreenOptions setValue:enabled forKey:@"enabled"]; From 37c31a3c92a6d0e884e0cf5732faeea56d115cc4 Mon Sep 17 00:00:00 2001 From: Ash Mishra Date: Mon, 20 Aug 2018 11:52:06 -0700 Subject: [PATCH 11/43] Improvements to urlFilePath so that it only does a /Documents/ folder check if the file does not exist; improvements to full-screen options for iOS to default autorotate, and to respect landscape / portrait masks --- ios/Video/RCTVideo.m | 12 +++++++----- ios/Video/RCTVideoPlayerViewController.m | 10 ++++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 8732f768..da1cbe31 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -370,16 +370,18 @@ static int const RCTVideoUnset = -1; } - (NSURL*) urlFilePath:(NSString*) filepath { - if ([filepath containsString:@"file://"] && ![filepath containsString:@"/Documents/"]) { - return [NSURL URLWithString:filepath]; + + // check if the file exists at the specified location + if ([[NSFileManager defaultManager] fileExistsAtPath:filepath]) { + return [NSURL fileURLWithPath:filepath]; } - // code to support local caching + // if no file found, check if the file exists in the Document directory NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString* relativeFilePath = [filepath lastPathComponent]; // the file may be multiple levels below the documents directory - NSArray* fileComponents = [filepath componentsSeparatedByString:@"Documents/"]; - if (fileComponents.count > 1) { + NSArray* fileComponents = [filepath componentsSeparatedByString:@"/Documents/"]; + if (fileComponents.count>1) { relativeFilePath = [fileComponents objectAtIndex:1]; } diff --git a/ios/Video/RCTVideoPlayerViewController.m b/ios/Video/RCTVideoPlayerViewController.m index b79a2de2..fe8cb171 100644 --- a/ios/Video/RCTVideoPlayerViewController.m +++ b/ios/Video/RCTVideoPlayerViewController.m @@ -9,7 +9,7 @@ - (id)init { self = [super init]; if (self) { - self.autorotate = false; + self.autorotate = true; // autorotate should be true by default } return self; } @@ -27,12 +27,18 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations { + if ([self.preferredOrientation.lowercaseString isEqualToString:@"landscape"]) { + return UIInterfaceOrientationMaskLandscape; + } + else if ([self.preferredOrientation.lowercaseString isEqualToString:@"portrait"]) { + return UIInterfaceOrientationMaskPortrait; + } return UIInterfaceOrientationMaskAll; } - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { if ([self.preferredOrientation.lowercaseString isEqualToString:@"landscape"]) { - return UIInterfaceOrientationLandscapeLeft; + return UIInterfaceOrientationLandscapeRight; } else if ([self.preferredOrientation.lowercaseString isEqualToString:@"portrait"]) { return UIInterfaceOrientationPortrait; From 77c48c9dfc3d3718f9319705c51bdebf86948af9 Mon Sep 17 00:00:00 2001 From: Ash Mishra Date: Mon, 27 Aug 2018 10:42:49 -0700 Subject: [PATCH 12/43] added an onCaptionsDeviceSettings event --- Video.js | 8 ++++++++ ios/Video/RCTVideo.h | 1 + ios/Video/RCTVideo.m | 11 ++++++++--- ios/Video/RCTVideoManager.m | 1 + 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Video.js b/Video.js index 58a7b9c1..2f2f3379 100644 --- a/Video.js +++ b/Video.js @@ -151,6 +151,12 @@ export default class Video extends Component { } }; + _onCaptionsDeviceSetting = (event) => { + if (this.props.onCaptionsDeviceSetting) { + this.props.onCaptionsDeviceSetting(event.nativeEvent); + } + }; + _onPlaybackStalled = (event) => { if (this.props.onPlaybackStalled) { this.props.onPlaybackStalled(event.nativeEvent); @@ -241,6 +247,7 @@ export default class Video extends Component { onVideoFullscreenPlayerWillDismiss: this._onFullscreenPlayerWillDismiss, onVideoFullscreenPlayerDidDismiss: this._onFullscreenPlayerDidDismiss, onReadyForDisplay: this._onReadyForDisplay, + onCaptionsDeviceSetting: this._onCaptionsDeviceSetting, onPlaybackStalled: this._onPlaybackStalled, onPlaybackResume: this._onPlaybackResume, onPlaybackRateChange: this._onPlaybackRateChange, @@ -379,6 +386,7 @@ Video.propTypes = { onFullscreenPlayerWillDismiss: PropTypes.func, onFullscreenPlayerDidDismiss: PropTypes.func, onReadyForDisplay: PropTypes.func, + onCaptionsDeviceSetting: PropTypes.func, onPlaybackStalled: PropTypes.func, onPlaybackResume: PropTypes.func, onPlaybackRateChange: PropTypes.func, diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index b2296c5d..23d420d8 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -32,6 +32,7 @@ @property (nonatomic, copy) RCTBubblingEventBlock onVideoFullscreenPlayerWillDismiss; @property (nonatomic, copy) RCTBubblingEventBlock onVideoFullscreenPlayerDidDismiss; @property (nonatomic, copy) RCTBubblingEventBlock onReadyForDisplay; +@property (nonatomic, copy) RCTBubblingEventBlock onCaptionsDeviceSetting; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackStalled; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackResume; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackRateChange; diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index da1cbe31..f894d911 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -68,6 +68,7 @@ static int const RCTVideoUnset = -1; #if __has_include() RCTVideoCache * _videoCache; #endif + BOOL _deviceCaptionsEnabled; } - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher @@ -364,6 +365,12 @@ static int const RCTVideoUnset = -1; @"target": self.reactTag }); } + + if (self.onCaptionsDeviceSetting) { + self.onCaptionsDeviceSetting(@{@"deviceCaptionsEnabled": [NSNumber numberWithBool:_deviceCaptionsEnabled], + @"target": self.reactTag + }); + } }]; }); _videoLoadStarted = YES; @@ -966,9 +973,7 @@ static int const RCTVideoUnset = -1; // in the situation that a selected text track is not available (eg. specifies a textTrack not available) if (![type isEqualToString:@"disabled"] && selectedTrackIndex == RCTVideoUnset) { - CFArrayRef captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser); - NSArray *captionSettings = (__bridge NSArray*)captioningMediaCharacteristics; - if ([captionSettings containsObject:AVMediaCharacteristicTranscribesSpokenDialogForAccessibility]) { + if (_deviceCaptionsEnabled) { selectedTrackIndex = 0; // If we can't find a match, use the first available track NSString *systemLanguage = [[NSLocale preferredLanguages] firstObject]; for (int i = 0; i < textTracks.count; ++i) { diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 9d902392..683238a1 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -54,6 +54,7 @@ RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerDidPresent, RCTBubblingEventBloc RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerWillDismiss, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerDidDismiss, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onReadyForDisplay, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onCaptionsDeviceSetting, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackStalled, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTBubblingEventBlock); From 443bf88c63d315bbe82501b8c662f64a4d40c439 Mon Sep 17 00:00:00 2001 From: Ash Mishra Date: Tue, 4 Sep 2018 15:44:19 -0700 Subject: [PATCH 13/43] Adds fullscreenOptions to iOS --- README.md | 29 ++++++++ ios/Video/RCTVideo.m | 156 +++++++++++++++++++++---------------------- 2 files changed, 107 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index c1dd0f0f..a1bfd118 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,8 @@ var styles = StyleSheet.create({ * [allowsExternalPlayback](#allowsexternalplayback) * [audioOnly](#audioonly) * [bufferConfig](#bufferconfig) +* [fullscreen (deprecated)](#fullscreen) +* [fullscreenOptions](#fullscreenOptions) * [ignoreSilentSwitch](#ignoresilentswitch) * [muted](#muted) * [paused](#paused) @@ -305,6 +307,33 @@ bufferConfig={{ Platforms: Android ExoPlayer +#### fullscreen (deprecated) + +Controls whether the player enters fullscreen on play. Use fullscreenOptions for extended behaviour. + +Platforms: iOS + +#### fullscreenOptions + +Controls behaviour of the player entering fullscreen, such as forcing landscape playback on portrait devices + +Property | Type | Description +--- | --- | --- +enabled | boolean | determines whether to enter fullscreen on video play +preferredOrientation | landscape, portrait, default | Defaults to the current device orientation; otherwise will force fullscreen video playback into landscape or portrait +autorotate | boolean | determines whether the video player will rotate to the preferredOrientation automatically + +Example with default values +``` +fullscreenOptions={{ + enabled: false, + preferredOrientation: 'default' + autorotate: true +}} +``` + +Platforms: iOS + #### ignoreSilentSwitch Controls the iOS silent switch behavior * **"inherit" (default)** - Use the default AVPlayer behavior diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index da1cbe31..9d68ad8f 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -119,19 +119,19 @@ static int const RCTVideoUnset = -1; - (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem { - RCTVideoPlayerViewController* playerLayer= [[RCTVideoPlayerViewController alloc] init]; - playerLayer.showsPlaybackControls = YES; - playerLayer.rctDelegate = self; - - if (_fullscreenOptions) { - playerLayer.preferredOrientation = [RCTConvert NSString:[_fullscreenOptions objectForKey:@"preferredOrientation"]]; - playerLayer.autorotate = [RCTConvert BOOL:[_fullscreenOptions objectForKey:@"autorotate"]]; - } - - playerLayer.view.frame = self.bounds; - playerLayer.player = player; - playerLayer.view.frame = self.bounds; - return playerLayer; + RCTVideoPlayerViewController* playerLayer= [[RCTVideoPlayerViewController alloc] init]; + playerLayer.showsPlaybackControls = YES; + playerLayer.rctDelegate = self; + + if (_fullscreenOptions) { + playerLayer.preferredOrientation = [RCTConvert NSString:[_fullscreenOptions objectForKey:@"preferredOrientation"]]; + playerLayer.autorotate = [RCTConvert BOOL:[_fullscreenOptions objectForKey:@"autorotate"]]; + } + + playerLayer.view.frame = self.bounds; + playerLayer.player = player; + playerLayer.view.frame = self.bounds; + return playerLayer; } /* --------------------------------------------------------- @@ -222,11 +222,11 @@ static int const RCTVideoUnset = -1; - (void)audioRouteChanged:(NSNotification *)notification { - NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; - NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; - if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { - self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); - } + NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; + NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; + if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { + self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); + } } #pragma mark - Progress @@ -381,7 +381,7 @@ static int const RCTVideoUnset = -1; NSString* relativeFilePath = [filepath lastPathComponent]; // the file may be multiple levels below the documents directory NSArray* fileComponents = [filepath componentsSeparatedByString:@"/Documents/"]; - if (fileComponents.count>1) { + if (fileComponents.count > 1) { relativeFilePath = [fileComponents objectAtIndex:1]; } @@ -729,8 +729,8 @@ static int const RCTVideoUnset = -1; - (void)setAllowsExternalPlayback:(BOOL)allowsExternalPlayback { - _allowsExternalPlayback = allowsExternalPlayback; - _player.allowsExternalPlayback = _allowsExternalPlayback; + _allowsExternalPlayback = allowsExternalPlayback; + _player.allowsExternalPlayback = _allowsExternalPlayback; } - (void)setPlayWhenInactive:(BOOL)playWhenInactive @@ -863,52 +863,52 @@ static int const RCTVideoUnset = -1; - (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)characteristic withCriteria:(NSDictionary *)criteria { - NSString *type = criteria[@"type"]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:characteristic]; - AVMediaSelectionOption *mediaOption; + NSString *type = criteria[@"type"]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:characteristic]; + AVMediaSelectionOption *mediaOption; - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { - NSString *value = criteria[@"value"]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *optionValue; - if ([type isEqualToString:@"language"]) { - optionValue = [currentOption extendedLanguageTag]; - } else { - optionValue = [[[currentOption commonMetadata] - valueForKey:@"value"] - objectAtIndex:0]; - } - if ([value isEqualToString:optionValue]) { - mediaOption = currentOption; - break; + if ([type isEqualToString:@"disabled"]) { + // Do nothing. We want to ensure option is nil + } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { + NSString *value = criteria[@"value"]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *optionValue; + if ([type isEqualToString:@"language"]) { + optionValue = [currentOption extendedLanguageTag]; + } else { + optionValue = [[[currentOption commonMetadata] + valueForKey:@"value"] + objectAtIndex:0]; + } + if ([value isEqualToString:optionValue]) { + mediaOption = currentOption; + break; + } } + //} else if ([type isEqualToString:@"default"]) { + // option = group.defaultOption; */ + } else if ([type isEqualToString:@"index"]) { + if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { + int index = [criteria[@"value"] intValue]; + if (group.options.count > index) { + mediaOption = [group.options objectAtIndex:index]; + } + } + } else { // default. invalid type or "system" + [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; + return; } - //} else if ([type isEqualToString:@"default"]) { - // option = group.defaultOption; */ - } else if ([type isEqualToString:@"index"]) { - if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [criteria[@"value"] intValue]; - if (group.options.count > index) { - mediaOption = [group.options objectAtIndex:index]; - } - } - } else { // default. invalid type or "system" - [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; - return; - } - // If a match isn't found, option will be nil and text tracks will be disabled - [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; + // If a match isn't found, option will be nil and text tracks will be disabled + [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; } - (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack { - _selectedAudioTrack = selectedAudioTrack; - [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible - withCriteria:_selectedAudioTrack]; + _selectedAudioTrack = selectedAudioTrack; + [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible + withCriteria:_selectedAudioTrack]; } - (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack { @@ -1043,25 +1043,25 @@ static int const RCTVideoUnset = -1; - (NSArray *)getAudioTrackInfo { - NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *title = @""; - NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; - if (values.count > 0) { - title = [values objectAtIndex:0]; + NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *title = @""; + NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; + if (values.count > 0) { + title = [values objectAtIndex:0]; + } + NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; + NSDictionary *audioTrack = @{ + @"index": [NSNumber numberWithInt:i], + @"title": title, + @"language": language + }; + [audioTracks addObject:audioTrack]; } - NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; - NSDictionary *audioTrack = @{ - @"index": [NSNumber numberWithInt:i], - @"title": title, - @"language": language - }; - [audioTracks addObject:audioTrack]; - } - return audioTracks; + return audioTracks; } - (NSArray *)getTextTrackInfo From 33d9acdf5de831198f3a36202a6e8bbc565da260 Mon Sep 17 00:00:00 2001 From: Matthew Herz Date: Thu, 6 Sep 2018 12:56:36 -0700 Subject: [PATCH 14/43] Fix missing TargetApi import The build was producing a "cannot find symbol class TargetApi" error without that line. --- android/src/main/java/com/brentvatne/react/ReactVideoView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/android/src/main/java/com/brentvatne/react/ReactVideoView.java b/android/src/main/java/com/brentvatne/react/ReactVideoView.java index b7c2eec5..3ff17a1f 100644 --- a/android/src/main/java/com/brentvatne/react/ReactVideoView.java +++ b/android/src/main/java/com/brentvatne/react/ReactVideoView.java @@ -1,6 +1,7 @@ package com.brentvatne.react; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.app.Activity; import android.content.res.AssetFileDescriptor; import android.graphics.Matrix; From bbacb953458fffc38cba846d1e5b27e3066e78f1 Mon Sep 17 00:00:00 2001 From: David Narbutovich Date: Wed, 12 Sep 2018 11:27:36 +0300 Subject: [PATCH 15/43] Update TextTrackType.js Remove not needed import --- TextTrackType.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/TextTrackType.js b/TextTrackType.js index caff65a9..9dc14077 100644 --- a/TextTrackType.js +++ b/TextTrackType.js @@ -1,5 +1,3 @@ -import keyMirror from 'keymirror'; - export default { SRT: 'application/x-subrip', TTML: 'application/ttml+xml', From a135dd346e5e847e1f0d4c864ea57a67881a3848 Mon Sep 17 00:00:00 2001 From: David Narbutovich Date: Wed, 12 Sep 2018 23:01:28 +0300 Subject: [PATCH 16/43] Reduce package size --- .npmignore | 1 - package.json | 11 ++++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 .npmignore diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 46b411bf..00000000 --- a/.npmignore +++ /dev/null @@ -1 +0,0 @@ -/examples diff --git a/package.json b/package.json index 3051b567..cfef620d 100644 --- a/package.json +++ b/package.json @@ -44,5 +44,14 @@ "android": { "sourceDir": "./android-exoplayer" } - } + }, + "files":[ + "android-exoplayer", + "android", + "ios", + "windows", + "TextTrackType.js", + "react-native-video.podspec", + "VideoResizeMode.js" + ] } \ No newline at end of file From 5e360608af485bdec2ddaf61b782e4c5a76d63c5 Mon Sep 17 00:00:00 2001 From: Hampton Maxwell Date: Wed, 12 Sep 2018 19:38:11 -0700 Subject: [PATCH 17/43] Return unset track index when there are no track available (#1190) --- .../java/com/brentvatne/exoplayer/ReactExoplayerView.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index e098c72a..0c1a178f 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -830,6 +830,10 @@ class ReactExoplayerView extends FrameLayout implements } private int getTrackIndexForDefaultLocale(TrackGroupArray groups) { + if (groups.length == 0) { // Avoid a crash if we try to select a non-existant group + return C.INDEX_UNSET; + } + int trackIndex = 0; // default if no match String locale2 = Locale.getDefault().getLanguage(); // 2 letter code String locale3 = Locale.getDefault().getISO3Language(); // 3 letter code From b0c0ac10f437a40b4d7d6184d21cdbbdea2886ed Mon Sep 17 00:00:00 2001 From: Hampton Maxwell Date: Wed, 12 Sep 2018 19:42:11 -0700 Subject: [PATCH 18/43] Avoid crash on ExoPlayer when there are no tracks to choose from --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ac26914..2bc5b674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Support video caching for iOS [#955](https://github.com/react-native-community/react-native-video/pull/955) * Video caching cleanups [#1172](https://github.com/react-native-community/react-native-video/pull/1172) * Add ipod-library support [#926](https://github.com/react-native-community/react-native-video/pull/926/files) +* Fix crash on ExoPlayer when there are no audio tracks [#1233](https://github.com/react-native-community/react-native-video/pull/1233) ### Version 3.2.0 * Basic fullscreen support for Android MediaPlayer [#1138](https://github.com/react-native-community/react-native-video/pull/1138) From d7efcf315faeb7711b85f639d06030e726a7fd39 Mon Sep 17 00:00:00 2001 From: Artur Jaworski Date: Thu, 13 Sep 2018 14:06:12 +0200 Subject: [PATCH 19/43] introducing onExternalPlaybackActiveChange --- Video.js | 8 ++++++++ ios/Video/RCTVideo.h | 1 + ios/Video/RCTVideo.m | 20 ++++++++++++++++++++ ios/Video/RCTVideoManager.m | 1 + 4 files changed, 30 insertions(+) diff --git a/Video.js b/Video.js index e8203b8b..ad9919b7 100644 --- a/Video.js +++ b/Video.js @@ -172,6 +172,12 @@ export default class Video extends Component { this.props.onPlaybackRateChange(event.nativeEvent); } }; + + _onExternalPlaybackChange = (event) => { + if (this.props.onExternalPlaybackChange) { + this.props.onExternalPlaybackChange(event.nativeEvent); + } + } _onAudioBecomingNoisy = () => { if (this.props.onAudioBecomingNoisy) { @@ -246,6 +252,7 @@ export default class Video extends Component { onPlaybackRateChange: this._onPlaybackRateChange, onAudioFocusChanged: this._onAudioFocusChanged, onAudioBecomingNoisy: this._onAudioBecomingNoisy, + onExternalPlaybackChange: this._onExternalPlaybackChange, }); if (this.props.poster && this.state.showPoster) { @@ -379,6 +386,7 @@ Video.propTypes = { onPlaybackRateChange: PropTypes.func, onAudioFocusChanged: PropTypes.func, onAudioBecomingNoisy: PropTypes.func, + onExternalPlaybackChange: PropTypes.func, /* Required by react-native */ scaleX: PropTypes.number, diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index b2296c5d..472cb2ff 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -35,6 +35,7 @@ @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackStalled; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackResume; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackRateChange; +@property (nonatomic, copy) RCTBubblingEventBlock onExternalPlaybackChange; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 9fe30b63..45389799 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -12,6 +12,7 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; static NSString *const readyForDisplayKeyPath = @"readyForDisplay"; static NSString *const playbackRate = @"rate"; static NSString *const timedMetadata = @"timedMetadata"; +static NSString *const externalPlaybackActive = @"externalPlaybackActive"; static int const RCTVideoUnset = -1; @@ -35,6 +36,7 @@ static int const RCTVideoUnset = -1; /* Required to publish events */ RCTEventDispatcher *_eventDispatcher; BOOL _playbackRateObserverRegistered; + BOOL _isExternalPlaybackActiveObserverRegistered; BOOL _videoLoadStarted; bool _pendingSeek; @@ -74,6 +76,7 @@ static int const RCTVideoUnset = -1; _eventDispatcher = eventDispatcher; _playbackRateObserverRegistered = NO; + _isExternalPlaybackActiveObserverRegistered = NO; _playbackStalled = NO; _rate = 1.0; _volume = 1.0; @@ -333,12 +336,19 @@ static int const RCTVideoUnset = -1; [_player removeObserver:self forKeyPath:playbackRate context:nil]; _playbackRateObserverRegistered = NO; } + if (_isExternalPlaybackActiveObserverRegistered) { + [_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; + _isExternalPlaybackActiveObserverRegistered = NO; + } _player = [AVPlayer playerWithPlayerItem:_playerItem]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; [_player addObserver:self forKeyPath:playbackRate options:0 context:nil]; _playbackRateObserverRegistered = YES; + + [_player addObserver:self forKeyPath:externalPlaybackActive options:0 context:nil]; + _isExternalPlaybackActiveObserverRegistered = YES; [self addPlayerTimeObserver]; @@ -646,6 +656,12 @@ static int const RCTVideoUnset = -1; _playbackStalled = NO; } } + else if([keyPath isEqualToString:externalPlaybackActive]) { + if(self.onExternalPlaybackChange) { + self.onExternalPlaybackChange(@{@"isExternalPlaybackActive": [NSNumber numberWithBool:_player.isExternalPlaybackActive], + @"target": self.reactTag}); + } + } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } @@ -1290,6 +1306,10 @@ static int const RCTVideoUnset = -1; [_player removeObserver:self forKeyPath:playbackRate context:nil]; _playbackRateObserverRegistered = NO; } + if (_isExternalPlaybackActiveObserverRegistered) { + [_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; + _isExternalPlaybackActiveObserverRegistered = NO; + } _player = nil; [self removePlayerLayer]; diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index e0e0162e..190a1663 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -56,6 +56,7 @@ RCT_EXPORT_VIEW_PROPERTY(onReadyForDisplay, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackStalled, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onExternalPlaybackChange, RCTBubblingEventBlock); - (NSDictionary *)constantsToExport { From 5e2b49c34377c10668681ea4a232580a03e189d5 Mon Sep 17 00:00:00 2001 From: Artur Jaworski Date: Tue, 18 Sep 2018 09:45:12 +0200 Subject: [PATCH 20/43] readme update --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index c1dd0f0f..f0ea1091 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,7 @@ var styles = StyleSheet.create({ * [onLoadStart](#onloadstart) * [onProgress](#onprogress) * [onTimedMetadata](#ontimedmetadata) +* [onExternalPlaybackChange](#onexternalplaybackchange) ### Methods * [dismissFullscreenPlayer](#dismissfullscreenplayer) @@ -721,6 +722,24 @@ Support for timed metadata on Android MediaPlayer is limited at best and only co Platforms: Android ExoPlayer, Android MediaPlayer, iOS +#### onExternalPlaybackChange +Callback function that is called when external playback mode for current playing video has changed. Mostly useful when connecting/disconnecting to Apple TV – it's called on connection/disconnection. + +Payload: + +Property | Type | Description +--- | --- | --- +isExternalPlaybackActive | boolean | Boolean indicating is external playback mode is active + +Example: +``` +{ + isExternalPlaybackActive: true +} +``` + +Platforms: iOS + ### Methods Methods operate on a ref to the Video element. You can create a ref using code like: ``` From ea4c732c9593e5db24b1d46d6284a7977d5001f7 Mon Sep 17 00:00:00 2001 From: Hampton Maxwell Date: Sat, 22 Sep 2018 15:01:51 -0700 Subject: [PATCH 21/43] Reduce package size --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bc5b674..60460e2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ * Video caching cleanups [#1172](https://github.com/react-native-community/react-native-video/pull/1172) * Add ipod-library support [#926](https://github.com/react-native-community/react-native-video/pull/926/files) * Fix crash on ExoPlayer when there are no audio tracks [#1233](https://github.com/react-native-community/react-native-video/pull/1233) +* Reduce package size [#1231](https://github.com/react-native-community/react-native-video/pull/1231) +* Remove unnecessary import in TextTrackType [#1229](https://github.com/react-native-community/react-native-video/pull/1229) ### Version 3.2.0 * Basic fullscreen support for Android MediaPlayer [#1138](https://github.com/react-native-community/react-native-video/pull/1138) From 584559ea7599e90eb294020c2db888f11fe2fde1 Mon Sep 17 00:00:00 2001 From: Hampton Maxwell Date: Sat, 22 Sep 2018 15:03:22 -0700 Subject: [PATCH 22/43] Bump version to 3.2.1 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3051b567..13e79075 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-video", - "version": "3.2.0", + "version": "3.2.1", "description": "A