From 412e1d0e3300dfd40ac38f1cdec2fa56a0c9c2c8 Mon Sep 17 00:00:00 2001 From: Frikkie Snyman Date: Wed, 28 Nov 2018 10:44:19 +0100 Subject: [PATCH 1/5] Implement ios bufferConfig --- README.md | 24 +++++++------- ios/Video/RCTVideo.m | 66 ++++++++++++++++++++++++++++++------- ios/Video/RCTVideoManager.m | 1 + 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index a7c8430e50..6239ec3912 100644 --- a/README.md +++ b/README.md @@ -334,17 +334,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. -Example with default values: -``` -bufferConfig={{ - minBufferMs: 15000, - maxBufferMs: 50000, - bufferForPlaybackMs: 2500, - bufferForPlaybackAfterRebufferMs: 5000 -}} -``` - -Platforms: Android ExoPlayer +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: Example with default values: + ``` ``` + bufferConfig={{ bufferConfig={{ + minBufferMs: 15000, minBufferMs: 15000, // not supported on iOS + maxBufferMs: 50000, maxBufferMs: 50000, // not supported on iOS + bufferForPlaybackMs: 2500, bufferForPlaybackMs: 2500, + bufferForPlaybackAfterRebufferMs: 5000 bufferForPlaybackAfterRebufferMs: 5000 + }} }} + ``` ``` + + Platforms: Android ExoPlayer Platforms: Android ExoPlayer, iOS #### controls Determines whether to show player controls. diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index a56b08a2aa..cd3fe1f87d 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"; @@ -47,6 +48,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; @@ -58,6 +64,7 @@ @implementation RCTVideo NSArray * _textTracks; NSDictionary * _selectedTextTrack; NSDictionary * _selectedAudioTrack; + NSDictionary * _bufferConfig; BOOL _playbackStalled; BOOL _playInBackground; BOOL _playWhenInactive; @@ -96,6 +103,9 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher _playInBackground = false; _allowsExternalPlayback = YES; _playWhenInactive = false; + _playbackStarted = NO; + _seeked = NO; + _previousTime = 0.0; _ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey #if __has_include() _videoCache = [RCTVideoCache sharedInstance]; @@ -252,15 +262,23 @@ - (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; } } @@ -304,6 +322,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; } @@ -317,6 +336,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; } @@ -645,6 +665,26 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N } _playerBufferEmpty = NO; self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag}); + } else if ([keyPath isEqualToString:loadedTimeRangesKeyPath]) { + DebugLog(@"frikkie: loadedTimeRangesKeyPath"); + if (_bufferConfig) { + DebugLog(@"frikkie: bufferConfig exists"); + 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 + } + DebugLog(@"frikkie: buffered: %f, threshold %f", buffered, threshold); + if (threshold > 0.0 && buffered >= threshold && !_paused) { + [_player playImmediatelyAtRate:1]; + } + } } } else if (object == _playerLayer) { if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey]) { @@ -821,7 +861,7 @@ - (void)setSeek:(NSDictionary *)info @"target": self.reactTag}); } }]; - + _seeked = YES; _pendingSeek = false; } @@ -859,7 +899,8 @@ - (void)applyModifiers [_player setVolume:_volume]; [_player setMuted:NO]; } - + + [self setBufferConfig:_bufferConfig]; [self setSelectedAudioTrack:_selectedAudioTrack]; [self setSelectedTextTrack:_selectedTextTrack]; [self setResizeMode:_resizeMode]; @@ -917,6 +958,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 9823dcfb94..694ade091e 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -40,6 +40,7 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_VIEW_PROPERTY(fullscreenOrientation, NSString); RCT_EXPORT_VIEW_PROPERTY(filter, NSString); 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); From ad41ec55edc537ff190522d9ec7825bdcb2e85b0 Mon Sep 17 00:00:00 2001 From: Frikkie Snyman <10909252+FrikkieSnyman@users.noreply.github.com> Date: Wed, 28 Nov 2018 10:59:25 +0100 Subject: [PATCH 2/5] Update README.md --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6239ec3912..55be6f8b99 100644 --- a/README.md +++ b/README.md @@ -336,17 +336,17 @@ This prop should only be set when you are setting the source, changing it after 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: Example with default values: - ``` ``` - bufferConfig={{ bufferConfig={{ - minBufferMs: 15000, minBufferMs: 15000, // not supported on iOS - maxBufferMs: 50000, maxBufferMs: 50000, // not supported on iOS - bufferForPlaybackMs: 2500, bufferForPlaybackMs: 2500, - bufferForPlaybackAfterRebufferMs: 5000 bufferForPlaybackAfterRebufferMs: 5000 - }} }} - ``` ``` +Example with default values: +``` +bufferConfig={{ + minBufferMs: 15000, // not supported on iOS + maxBufferMs: 50000, // not supported on iOS + bufferForPlaybackMs: 2500, + bufferForPlaybackAfterRebufferMs: 5000 +}} +``` - Platforms: Android ExoPlayer Platforms: Android ExoPlayer, iOS +Platforms: Android ExoPlayer, iOS #### controls Determines whether to show player controls. From 34f5b84c52ba1166a4ac22e6af238bc3f4cf4b8b Mon Sep 17 00:00:00 2001 From: Frikkie Snyman <10909252+FrikkieSnyman@users.noreply.github.com> Date: Wed, 28 Nov 2018 11:00:06 +0100 Subject: [PATCH 3/5] Update RCTVideo.m --- ios/Video/RCTVideo.m | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index cd3fe1f87d..05ecc6b0ba 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -666,9 +666,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N _playerBufferEmpty = NO; self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag}); } else if ([keyPath isEqualToString:loadedTimeRangesKeyPath]) { - DebugLog(@"frikkie: loadedTimeRangesKeyPath"); if (_bufferConfig) { - DebugLog(@"frikkie: bufferConfig exists"); double buffered = [[self calculatePlayableDuration] doubleValue] - [[NSNumber numberWithFloat:CMTimeGetSeconds(_player.currentTime)] doubleValue]; double threshold = 0.0; if (_bufferConfig[@"bufferForPlaybackAfterRebufferMs"]) { @@ -680,7 +678,6 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N double bufferForPlaybackMs = [_bufferConfig[@"bufferForPlaybackMs"] doubleValue]; threshold = bufferForPlaybackMs / 1000; // bufferForPlaybackMs } - DebugLog(@"frikkie: buffered: %f, threshold %f", buffered, threshold); if (threshold > 0.0 && buffered >= threshold && !_paused) { [_player playImmediatelyAtRate:1]; } @@ -1504,4 +1501,4 @@ - (NSString *)cacheDirectoryPath { return array[0]; } -@end \ No newline at end of file +@end From c1f770c75b4030124fdd3e464015457d9969800b Mon Sep 17 00:00:00 2001 From: Frikkie Snyman Date: Wed, 28 Nov 2018 11:13:02 +0100 Subject: [PATCH 4/5] playback at specified rate when buffer surpasses threshold --- ios/Video/RCTVideo.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 05ecc6b0ba..60cc39bdf5 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -679,7 +679,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N threshold = bufferForPlaybackMs / 1000; // bufferForPlaybackMs } if (threshold > 0.0 && buffered >= threshold && !_paused) { - [_player playImmediatelyAtRate:1]; + [_player playImmediatelyAtRate:_rate]; } } } From 3a43b5d5c2350ca12344758c7e349c4ab807525a Mon Sep 17 00:00:00 2001 From: Frikkie Snyman <10909252+FrikkieSnyman@users.noreply.github.com> Date: Wed, 28 Nov 2018 11:14:27 +0100 Subject: [PATCH 5/5] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 55be6f8b99..d80d9943c7 100644 --- a/README.md +++ b/README.md @@ -339,13 +339,13 @@ On iOS, only `bufferForPlaybackMs` and `bufferForPlaybackAfterRebufferMs` is sup Example with default values: ``` bufferConfig={{ - minBufferMs: 15000, // not supported on iOS - maxBufferMs: 50000, // not supported on iOS - bufferForPlaybackMs: 2500, - bufferForPlaybackAfterRebufferMs: 5000 + minBufferMs: 15000, // not supported on iOS + maxBufferMs: 50000, // not supported on iOS + bufferForPlaybackMs: 2500, + bufferForPlaybackAfterRebufferMs: 5000 }} ``` - + Platforms: Android ExoPlayer, iOS #### controls