diff --git a/README.md b/README.md index c1dd0f0ffe..36eee1b18b 100644 --- a/README.md +++ b/README.md @@ -293,17 +293,19 @@ playbackAfterRebufferMs | number | The default duration of media that must be bu 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 #### ignoreSilentSwitch Controls the iOS silent switch behavior diff --git a/ios/RCTVideo.m b/ios/RCTVideo.m index 2505bdb839..ae46d6d8d6 100644 --- a/ios/RCTVideo.m +++ b/ios/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"; @@ -45,6 +46,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; @@ -53,6 +59,7 @@ @implementation RCTVideo BOOL _paused; BOOL _repeat; BOOL _allowsExternalPlayback; + NSDictionary * _bufferConfig; NSArray * _textTracks; NSDictionary * _selectedTextTrack; NSDictionary * _selectedAudioTrack; @@ -86,6 +93,9 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher _allowsExternalPlayback = YES; _playWhenInactive = false; _ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey + _playbackStarted = NO; + _seeked = NO; + _previousTime = 0.0; _videoCache = [RCTVideoCache sharedInstance]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) @@ -236,16 +246,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], - @"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], + @"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; } /*! @@ -288,6 +306,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; } @@ -301,6 +320,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; } @@ -329,13 +349,13 @@ - (void)setSrc:(NSDictionary *)source [_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]; //Perform on next run loop, otherwise onVideoLoadStart is nil @@ -621,6 +641,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:1]; + } + } } } else if (object == _playerLayer) { if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey]) { @@ -791,7 +828,7 @@ - (void)setSeek:(NSDictionary *)info @"target": self.reactTag}); } }]; - + _seeked = YES; _pendingSeek = false; } @@ -830,6 +867,7 @@ - (void)applyModifiers [_player setMuted:NO]; } + [self setBufferConfig:_bufferConfig]; [self setSelectedAudioTrack:_selectedAudioTrack]; [self setSelectedTextTrack:_selectedTextTrack]; [self setResizeMode:_resizeMode]; @@ -888,6 +926,10 @@ - (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)character [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; } +- (void)setBufferConfig:(NSDictionary *)bufferConfig { + _bufferConfig = bufferConfig; +} + - (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack { _selectedAudioTrack = selectedAudioTrack; [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible diff --git a/ios/RCTVideoManager.m b/ios/RCTVideoManager.m index e0e0162e7b..1e0d12a1c5 100644 --- a/ios/RCTVideoManager.m +++ b/ios/RCTVideoManager.m @@ -38,6 +38,7 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_VIEW_PROPERTY(currentTime, float); RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL); RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float); +RCT_EXPORT_VIEW_PROPERTY(bufferConfig, NSDictionary); /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */ RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTBubblingEventBlock);