diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 2c8190ca6d..d097e78bfc 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -33,25 +33,27 @@ @implementation RCTVideo BOOL _playerLayerObserverSet; RCTVideoPlayerViewController *_playerViewController; NSURL *_videoURL; - + /* Required to publish events */ RCTEventDispatcher *_eventDispatcher; BOOL _playbackRateObserverRegistered; BOOL _isExternalPlaybackActiveObserverRegistered; 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; + float _maxBitRate; + BOOL _muted; BOOL _paused; BOOL _repeat; @@ -80,7 +82,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher { if ((self = [super init])) { _eventDispatcher = eventDispatcher; - + _playbackRateObserverRegistered = NO; _isExternalPlaybackActiveObserverRegistered = NO; _playbackStalled = NO; @@ -106,23 +108,23 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher 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; } @@ -132,7 +134,7 @@ - (RCTVideoPlayerViewController*)createPlayerViewController:(AVPlayer*)player viewController.showsPlaybackControls = YES; viewController.rctDelegate = self; viewController.preferredOrientation = _fullscreenOrientation; - + viewController.view.frame = self.bounds; viewController.player = player; viewController.view.frame = self.bounds; @@ -150,7 +152,7 @@ - (CMTime)playerItemDuration { return([playerItem duration]); } - + return(kCMTimeInvalid); } @@ -161,7 +163,7 @@ - (CMTimeRange)playerItemSeekableTimeRange { return [playerItem seekableTimeRanges].firstObject.CMTimeRangeValue; } - + return (kCMTimeRangeZero); } @@ -202,7 +204,7 @@ - (void)dealloc - (void)applicationWillResignActive:(NSNotification *)notification { if (_playInBackground || _playWhenInactive || _paused) return; - + [_player pause]; [_player setRate:0.0]; } @@ -242,18 +244,18 @@ - (void)sendProgressUpdate 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)], @@ -340,11 +342,12 @@ - (void)setSrc:(NSDictionary *)source _playerItem = playerItem; [self addPlayerItemObservers]; [self setFilter:_filterName]; - + [self setMaxBitRate:_maxBitRate]; + [_player pause]; [_playerViewController.view removeFromSuperview]; _playerViewController = nil; - + if (_playbackRateObserverRegistered) { [_player removeObserver:self forKeyPath:playbackRate context:nil]; _playbackRateObserverRegistered = NO; @@ -353,16 +356,16 @@ - (void)setSrc:(NSDictionary *)source [_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]; //Perform on next run loop, otherwise onVideoLoadStart is nil @@ -385,7 +388,7 @@ - (NSURL*) urlFilePath:(NSString*) filepath { if ([filepath containsString:@"file://"]) { return [NSURL URLWithString:filepath]; } - + // if no file found, check if the file exists in the Document directory NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString* relativeFilePath = [filepath lastPathComponent]; @@ -394,7 +397,7 @@ - (NSURL*) urlFilePath:(NSString*) filepath { if (fileComponents.count > 1) { relativeFilePath = [fileComponents objectAtIndex:1]; } - + NSString *path = [paths.firstObject stringByAppendingPathComponent:relativeFilePath]; if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { return [NSURL fileURLWithPath:path]; @@ -404,28 +407,31 @@ - (NSURL*) urlFilePath:(NSString*) filepath { - (void)playerItemPrepareText:(AVAsset *)asset assetOptions:(NSDictionary * __nullable)assetOptions withCallback:(void(^)(AVPlayerItem *))handler { - if (!_textTracks) { + if (!_textTracks || _textTracks.count==0) { handler([AVPlayerItem playerItemWithAsset:asset]); return; } + + // AVPlayer can't airplay AVMutableCompositions + _allowsExternalPlayback = NO; // sideload text tracks AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init]; - + AVAssetTrack *videoAsset = [asset tracksWithMediaType:AVMediaTypeVideo].firstObject; AVMutableCompositionTrack *videoCompTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; [videoCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) 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; @@ -464,7 +470,7 @@ - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlaye ? [NSURL URLWithString:uri] : [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]]; 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. @@ -530,7 +536,7 @@ - (void)playerItemForSourceUsingCache:(NSString *)uri assetOptions:(NSDictionary DVURLAsset *asset = [[DVURLAsset alloc] initWithURL:url options:options networkTimeout:10000]; asset.loaderDelegate = self; - + /* More granular code to have control over the DVURLAsset DVAssetLoaderDelegate *resourceLoaderDelegate = [[DVAssetLoaderDelegate alloc] initWithURL:url]; resourceLoaderDelegate.delegate = self; @@ -567,40 +573,40 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N for (AVMetadataItem *item in items) { NSString *value = (NSString *)item.value; NSString *identifier = item.identifier; - + if (![value isEqual: [NSNull null]]) { NSDictionary *dictionary = [[NSDictionary alloc] initWithObjects:@[value, identifier] forKeys:@[@"value", @"identifier"]]; - + [array addObject:dictionary]; } } - + self.onTimedMetadata(@{ @"target": self.reactTag, @"metadata": array }); } } - + if ([keyPath isEqualToString:statusKeyPath]) { // Handle player item status change. if (_playerItem.status == AVPlayerItemStatusReadyToPlay) { float duration = CMTimeGetSeconds(_playerItem.asset.duration); - + if (isnan(duration)) { duration = 0.0; } - + NSObject *width = @"undefined"; NSObject *height = @"undefined"; NSString *orientation = @"undefined"; - + if ([_playerItem.asset tracksWithMediaType:AVMediaTypeVideo].count > 0) { AVAssetTrack *videoTrack = [[_playerItem.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; width = [NSNumber numberWithFloat:videoTrack.naturalSize.width]; height = [NSNumber numberWithFloat:videoTrack.naturalSize.height]; CGAffineTransform preferredTransform = [videoTrack preferredTransform]; - + if ((videoTrack.naturalSize.width == preferredTransform.tx && videoTrack.naturalSize.height == preferredTransform.ty) || (preferredTransform.tx == 0 && preferredTransform.ty == 0)) @@ -610,7 +616,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N orientation = @"portrait"; } } - + if (self.onVideoLoad && _videoLoadStarted) { self.onVideoLoad(@{@"duration": [NSNumber numberWithFloat:duration], @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(_playerItem.currentTime)], @@ -630,7 +636,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N @"target": self.reactTag}); } _videoLoadStarted = NO; - + [self attachListeners]; [self applyModifiers]; } else if (_playerItem.status == AVPlayerItemStatusFailed && self.onVideoError) { @@ -690,7 +696,7 @@ - (void)attachListeners selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:[_player currentItem]]; - + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemPlaybackStalledNotification object:nil]; @@ -713,7 +719,7 @@ - (void)playerItemDidReachEnd:(NSNotification *)notification if(self.onVideoEnd) { self.onVideoEnd(@{@"target": self.reactTag}); } - + if (_repeat) { AVPlayerItem *item = [notification object]; [item seekToTime:kCMTimeZero]; @@ -774,7 +780,7 @@ - (void)setPaused:(BOOL)paused [_player play]; [_player setRate:_rate]; } - + _paused = paused; } @@ -796,19 +802,19 @@ - (void)setSeek:(NSDictionary *)info { NSNumber *seekTime = info[@"time"]; NSNumber *seekTolerance = info[@"tolerance"]; - + int timeScale = 1000; - + AVPlayerItem *item = _player.currentItem; if (item && item.status == AVPlayerItemStatusReadyToPlay) { // TODO check loadedTimeRanges - + CMTime cmSeekTime = CMTimeMakeWithSeconds([seekTime floatValue], timeScale); CMTime current = item.currentTime; // TODO figure out a good tolerance level CMTime tolerance = CMTimeMake([seekTolerance floatValue], timeScale); BOOL wasPaused = _paused; - + if (CMTimeCompare(current, cmSeekTime) != 0) { if (!wasPaused) [_player pause]; [_player seekToTime:cmSeekTime toleranceBefore:tolerance toleranceAfter:tolerance completionHandler:^(BOOL finished) { @@ -824,10 +830,10 @@ - (void)setSeek:(NSDictionary *)info @"target": self.reactTag}); } }]; - + _pendingSeek = false; } - + } else { // TODO: See if this makes sense and if so, actually implement it _pendingSeek = true; @@ -853,6 +859,12 @@ - (void)setVolume:(float)volume [self applyModifiers]; } +- (void)setMaxBitRate:(float) maxBitRate { + _maxBitRate = maxBitRate; + [self applyModifiers]; +} + + - (void)applyModifiers { if (_muted) { @@ -862,7 +874,9 @@ - (void)applyModifiers [_player setVolume:_volume]; [_player setMuted:NO]; } - + + _playerItem.preferredPeakBitRate = _maxBitRate; + [self setSelectedAudioTrack:_selectedAudioTrack]; [self setSelectedTextTrack:_selectedTextTrack]; [self setResizeMode:_resizeMode]; @@ -883,7 +897,7 @@ - (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)character 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"]) { @@ -916,7 +930,7 @@ - (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)character [_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]; } @@ -940,7 +954,7 @@ - (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack { - (void) setSideloadedText { NSString *type = _selectedTextTrack[@"type"]; NSArray *textTracks = [self getTextTrackInfo]; - + // The first few tracks will be audio & video track int firstTextIndex = 0; for (firstTextIndex = 0; firstTextIndex < _player.currentItem.tracks.count; ++firstTextIndex) { @@ -948,9 +962,9 @@ - (void) setSideloadedText { break; } } - + int selectedTrackIndex = RCTVideoUnset; - + if ([type isEqualToString:@"disabled"]) { // Do nothing. We want to ensure option is nil } else if ([type isEqualToString:@"language"]) { @@ -979,7 +993,7 @@ - (void) setSideloadedText { } } } - + // 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); @@ -996,7 +1010,7 @@ - (void) setSideloadedText { } } } - + for (int i = firstTextIndex; i < _player.currentItem.tracks.count; ++i) { BOOL isEnabled = NO; if (selectedTrackIndex != RCTVideoUnset) { @@ -1011,7 +1025,7 @@ -(void) setStreamingText { AVMediaSelectionGroup *group = [_player.currentItem.asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; AVMediaSelectionOption *mediaOption; - + if ([type isEqualToString:@"disabled"]) { // Do nothing. We want to ensure option is nil } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { @@ -1044,7 +1058,7 @@ -(void) setStreamingText { [_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]; } @@ -1052,7 +1066,7 @@ -(void) setStreamingText { - (void)setTextTracks:(NSArray*) textTracks; { _textTracks = textTracks; - + // in case textTracks was set after selectedTextTrack if (_selectedTextTrack) [self setSelectedTextTrack:_selectedTextTrack]; } @@ -1084,7 +1098,7 @@ - (NSArray *)getTextTrackInfo { // if sideloaded, textTracks will already be set if (_textTracks) return _textTracks; - + // if streaming video, we extract the text tracks NSMutableArray *textTracks = [[NSMutableArray alloc] init]; AVMediaSelectionGroup *group = [_player.currentItem.asset @@ -1122,7 +1136,7 @@ - (void)setFullscreen:(BOOL) fullscreen { } // Set presentation style to fullscreen [_playerViewController setModalPresentationStyle:UIModalPresentationFullScreen]; - + // Find the nearest view controller UIViewController *viewController = [self firstAvailableUIViewController]; if( !viewController ) @@ -1192,13 +1206,13 @@ - (void)usePlayerLayer _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; _playerLayer.frame = self.bounds; _playerLayer.needsDisplayOnBoundsChange = YES; - + // to prevent video from being animated when resizeMode is 'cover' // resize mode must be set before layer is added [self setResizeMode:_resizeMode]; [_playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil]; _playerLayerObserverSet = YES; - + [self.layer addSublayer:_playerLayer]; self.layer.needsDisplayOnBoundsChange = YES; } @@ -1226,7 +1240,7 @@ - (void)setControls:(BOOL)controls - (void)setProgressUpdateInterval:(float)progressUpdateInterval { _progressUpdateInterval = progressUpdateInterval; - + if (_timeObserver) { [self removePlayerTimeObserver]; [self addPlayerTimeObserver]; @@ -1277,11 +1291,10 @@ - (void)setFilter:(NSString *)filterName { } AVAsset *asset = _playerItem.asset; - + if (!asset) { return; } - // TODO: filters don't work for HLS, check & return CIFilter *filter = [CIFilter filterWithName:filterName]; _playerItem.videoComposition = [AVVideoComposition @@ -1312,7 +1325,7 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex { [self setControls:true]; } - + if( _controls ) { view.frame = self.bounds; @@ -1344,7 +1357,7 @@ - (void)layoutSubviews if( _controls ) { _playerViewController.view.frame = self.bounds; - + // also adjust all subviews of contentOverlayView for (UIView* subview in _playerViewController.contentOverlayView.subviews) { subview.frame = self.bounds; @@ -1373,18 +1386,18 @@ - (void)removeFromSuperview _isExternalPlaybackActiveObserverRegistered = NO; } _player = nil; - + [self removePlayerLayer]; - + [_playerViewController.view removeFromSuperview]; _playerViewController = nil; - + [self removePlayerTimeObserver]; [self removePlayerItemObservers]; - + _eventDispatcher = nil; [[NSNotificationCenter defaultCenter] removeObserver:self]; - + [super removeFromSuperview]; } @@ -1462,4 +1475,4 @@ - (NSString *)cacheDirectoryPath { return array[0]; } -@end +@end \ No newline at end of file