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