|
|
|
@@ -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(<react-native-video/RCTVideoCache.h>)
|
|
|
|
@@ -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];
|
|
|
|
|