diff --git a/README.md b/README.md index 1b6a913ec3..0d125070fd 100644 --- a/README.md +++ b/README.md @@ -379,17 +379,19 @@ bufferForPlaybackAfterRebufferMs | number | The default duration of media that m This prop should only be set when you are setting the source, changing it after the media is loaded will cause it to be reloaded. +On iOS, only `bufferForPlaybackMs` and `bufferForPlaybackAfterRebufferMs` is supported. If these values are not specified, or no `bufferConfig` is supplied, then the default AVPlayer buffering is used. This behaviour tries to determine whether the video item is likely to play through given the current buffer rate, and if so, the video starts to play. + Example with default values: ``` bufferConfig={{ - minBufferMs: 15000, - maxBufferMs: 50000, + minBufferMs: 15000, // not supported on iOS + maxBufferMs: 50000, // not supported on iOS bufferForPlaybackMs: 2500, bufferForPlaybackAfterRebufferMs: 5000 }} ``` -Platforms: Android ExoPlayer +Platforms: Android ExoPlayer, iOS #### currentPlaybackTime When playing an HLS live stream with a `EXT-X-PROGRAM-DATE-TIME` tag configured, then this property will contain the epoch value in msec. diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 8780f48f68..9e9a62529c 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -10,6 +10,7 @@ static NSString *const playbackLikelyToKeepUpKeyPath = @"playbackLikelyToKeepUp"; static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; static NSString *const readyForDisplayKeyPath = @"readyForDisplay"; +static NSString *const loadedTimeRangesKeyPath = @"loadedTimeRanges"; static NSString *const playbackRate = @"rate"; static NSString *const timedMetadata = @"timedMetadata"; static NSString *const externalPlaybackActive = @"externalPlaybackActive"; @@ -54,6 +55,11 @@ @implementation RCTVideo Float64 _progressUpdateInterval; BOOL _controls; id _timeObserver; + + /* For keeping track of buffer states */ + BOOL _playbackStarted; + BOOL _seeked; + Float64 _previousTime; /* Keep track of any modifiers, need to be applied after each play */ float _volume; @@ -68,6 +74,7 @@ @implementation RCTVideo NSArray * _textTracks; NSDictionary * _selectedTextTrack; NSDictionary * _selectedAudioTrack; + NSDictionary * _bufferConfig; BOOL _playbackStalled; BOOL _playInBackground; BOOL _preventsDisplaySleepDuringVideoPlayback; @@ -117,6 +124,10 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher _preferredForwardBufferDuration = 0.0f; _allowsExternalPlayback = YES; _playWhenInactive = false; + + _playbackStarted = NO; + _seeked = NO; + _previousTime = 0.0; _pictureInPicture = false; _ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey _mixWithOthers = @"inherit"; // inherit, mix, duck @@ -281,16 +292,24 @@ - (void)sendProgressUpdate [[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], - @"currentPlaybackTime": [NSNumber numberWithLongLong:[@(floor([currentPlaybackTime timeIntervalSince1970] * 1000)) longLongValue]], - @"target": self.reactTag, - @"seekableDuration": [self calculateSeekableDuration], - }); + if(currentTimeSecs >= 0) { + _playbackStarted = YES; + if (self.onVideoProgress) { + self.onVideoProgress(@{ + @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(currentTime)], + @"playableDuration": [self calculatePlayableDuration], + @"atValue": [NSNumber numberWithLongLong:currentTime.value], + @"atTimescale": [NSNumber numberWithInt:currentTime.timescale], + @"currentPlaybackTime": [NSNumber numberWithLongLong:[@(floor([currentPlaybackTime timeIntervalSince1970] * 1000)) longLongValue]], + @"target": self.reactTag, + @"seekableDuration": [self calculateSeekableDuration], + }); + } + if (_previousTime != currentTimeSecs) { + // video has progressed + _seeked = NO; // seeked has completed and video has enough data in buffer to play again + } + _previousTime = currentTimeSecs; } } @@ -334,6 +353,7 @@ - (void)addPlayerItemObservers [_playerItem addObserver:self forKeyPath:statusKeyPath options:0 context:nil]; [_playerItem addObserver:self forKeyPath:playbackBufferEmptyKeyPath options:0 context:nil]; [_playerItem addObserver:self forKeyPath:playbackLikelyToKeepUpKeyPath options:0 context:nil]; + [_playerItem addObserver:self forKeyPath:loadedTimeRangesKeyPath options:0 context:nil]; [_playerItem addObserver:self forKeyPath:timedMetadata options:NSKeyValueObservingOptionNew context:nil]; _playerItemObserversSet = YES; } @@ -347,6 +367,7 @@ - (void)removePlayerItemObservers [_playerItem removeObserver:self forKeyPath:statusKeyPath]; [_playerItem removeObserver:self forKeyPath:playbackBufferEmptyKeyPath]; [_playerItem removeObserver:self forKeyPath:playbackLikelyToKeepUpKeyPath]; + [_playerItem removeObserver:self forKeyPath:loadedTimeRangesKeyPath]; [_playerItem removeObserver:self forKeyPath:timedMetadata]; _playerItemObserversSet = NO; } @@ -715,6 +736,23 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N } _playerBufferEmpty = NO; self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag}); + } else if ([keyPath isEqualToString:loadedTimeRangesKeyPath]) { + if (_bufferConfig) { + double buffered = [[self calculatePlayableDuration] doubleValue] - [[NSNumber numberWithFloat:CMTimeGetSeconds(_player.currentTime)] doubleValue]; + double threshold = 0.0; + if (_bufferConfig[@"bufferForPlaybackAfterRebufferMs"]) { + double playbackAfterRebufferMs = [_bufferConfig[@"bufferForPlaybackAfterRebufferMs"] doubleValue]; + threshold = playbackAfterRebufferMs / 1000; // default to playbackAfterRebufferMs + } + if ((!_playbackStarted || _seeked) && _bufferConfig[@"bufferForPlaybackMs"]) { + // video is yet to start playback, or user has interrupted video with a seek event + double bufferForPlaybackMs = [_bufferConfig[@"bufferForPlaybackMs"] doubleValue]; + threshold = bufferForPlaybackMs / 1000; // bufferForPlaybackMs + } + if (threshold > 0.0 && buffered >= threshold && !_paused) { + [_player playImmediatelyAtRate:_rate]; + } + } } } else if (object == _player) { if([keyPath isEqualToString:playbackRate]) { @@ -1012,7 +1050,7 @@ - (void)setSeek:(NSDictionary *)info @"target": self.reactTag}); } }]; - + _seeked = YES; _pendingSeek = false; } @@ -1070,12 +1108,14 @@ - (void)applyModifiers [_player setMuted:NO]; } + [self setBufferConfig:_bufferConfig]; + if (@available(iOS 12.0, *)) { self->_player.preventsDisplaySleepDuringVideoPlayback = _preventsDisplaySleepDuringVideoPlayback; } else { // Fallback on earlier versions } - + [self setMaxBitRate:_maxBitRate]; [self setSelectedAudioTrack:_selectedAudioTrack]; [self setSelectedTextTrack:_selectedTextTrack]; @@ -1134,6 +1174,9 @@ - (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)character // If a match isn't found, option will be nil and text tracks will be disabled [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; } +- (void)setBufferConfig:(NSDictionary *)bufferConfig { + _bufferConfig = bufferConfig; +} - (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack { _selectedAudioTrack = selectedAudioTrack; diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 000a9e83af..381400a895 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -48,6 +48,7 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_VIEW_PROPERTY(filter, NSString); RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float); +RCT_EXPORT_VIEW_PROPERTY(bufferConfig, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL); /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */ RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTDirectEventBlock);