Skip to content

Commit 4cbc4c2

Browse files
Merge pull request #4 from relivecc/feat/ios-buffer-config
Feat/ios buffer config
2 parents 1f64231 + 48e51f7 commit 4cbc4c2

3 files changed

Lines changed: 60 additions & 15 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,17 +293,19 @@ playbackAfterRebufferMs | number | The default duration of media that must be bu
293293

294294
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.
295295

296+
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.
297+
296298
Example with default values:
297299
```
298300
bufferConfig={{
299-
minBufferMs: 15000,
300-
maxBufferMs: 50000,
301+
minBufferMs: 15000, // not supported on iOS
302+
maxBufferMs: 50000, // not supported on iOS
301303
bufferForPlaybackMs: 2500,
302304
bufferForPlaybackAfterRebufferMs: 5000
303305
}}
304306
```
305307

306-
Platforms: Android ExoPlayer
308+
Platforms: Android ExoPlayer, iOS
307309

308310
#### ignoreSilentSwitch
309311
Controls the iOS silent switch behavior

ios/RCTVideo.m

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
static NSString *const playbackLikelyToKeepUpKeyPath = @"playbackLikelyToKeepUp";
1111
static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty";
1212
static NSString *const readyForDisplayKeyPath = @"readyForDisplay";
13+
static NSString *const loadedTimeRangesKeyPath = @"loadedTimeRanges";
1314
static NSString *const playbackRate = @"rate";
1415
static NSString *const timedMetadata = @"timedMetadata";
1516

@@ -45,6 +46,11 @@ @implementation RCTVideo
4546
Float64 _progressUpdateInterval;
4647
BOOL _controls;
4748
id _timeObserver;
49+
50+
/* For keeping track of buffer states */
51+
BOOL _playbackStarted;
52+
BOOL _seeked;
53+
Float64 _previousTime;
4854

4955
/* Keep track of any modifiers, need to be applied after each play */
5056
float _volume;
@@ -53,6 +59,7 @@ @implementation RCTVideo
5359
BOOL _paused;
5460
BOOL _repeat;
5561
BOOL _allowsExternalPlayback;
62+
NSDictionary * _bufferConfig;
5663
NSArray * _textTracks;
5764
NSDictionary * _selectedTextTrack;
5865
NSDictionary * _selectedAudioTrack;
@@ -86,6 +93,9 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
8693
_allowsExternalPlayback = YES;
8794
_playWhenInactive = false;
8895
_ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey
96+
_playbackStarted = NO;
97+
_seeked = NO;
98+
_previousTime = 0.0;
8999
_videoCache = [RCTVideoCache sharedInstance];
90100
[[NSNotificationCenter defaultCenter] addObserver:self
91101
selector:@selector(applicationWillResignActive:)
@@ -236,16 +246,24 @@ - (void)sendProgressUpdate
236246

237247
[[NSNotificationCenter defaultCenter] postNotificationName:@"RCTVideo_progress" object:nil userInfo:@{@"progress": [NSNumber numberWithDouble: currentTimeSecs / duration]}];
238248

239-
if( currentTimeSecs >= 0 && self.onVideoProgress) {
240-
self.onVideoProgress(@{
241-
@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(currentTime)],
242-
@"playableDuration": [self calculatePlayableDuration],
243-
@"atValue": [NSNumber numberWithLongLong:currentTime.value],
244-
@"atTimescale": [NSNumber numberWithInt:currentTime.timescale],
245-
@"target": self.reactTag,
246-
@"seekableDuration": [self calculateSeekableDuration],
247-
});
249+
if( currentTimeSecs >= 0) {
250+
_playbackStarted = YES;
251+
if (self.onVideoProgress) {
252+
self.onVideoProgress(@{
253+
@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(currentTime)],
254+
@"playableDuration": [self calculatePlayableDuration],
255+
@"atValue": [NSNumber numberWithLongLong:currentTime.value],
256+
@"atTimescale": [NSNumber numberWithInt:currentTime.timescale],
257+
@"target": self.reactTag,
258+
@"seekableDuration": [self calculateSeekableDuration],
259+
});
260+
}
248261
}
262+
if (_previousTime != currentTimeSecs) {
263+
// video has progressed
264+
_seeked = NO; // seeked has completed and video has enough data in buffer to play again
265+
}
266+
_previousTime = currentTimeSecs;
249267
}
250268

251269
/*!
@@ -288,6 +306,7 @@ - (void)addPlayerItemObservers
288306
[_playerItem addObserver:self forKeyPath:statusKeyPath options:0 context:nil];
289307
[_playerItem addObserver:self forKeyPath:playbackBufferEmptyKeyPath options:0 context:nil];
290308
[_playerItem addObserver:self forKeyPath:playbackLikelyToKeepUpKeyPath options:0 context:nil];
309+
[_playerItem addObserver:self forKeyPath:loadedTimeRangesKeyPath options:0 context:nil];
291310
[_playerItem addObserver:self forKeyPath:timedMetadata options:NSKeyValueObservingOptionNew context:nil];
292311
_playerItemObserversSet = YES;
293312
}
@@ -301,6 +320,7 @@ - (void)removePlayerItemObservers
301320
[_playerItem removeObserver:self forKeyPath:statusKeyPath];
302321
[_playerItem removeObserver:self forKeyPath:playbackBufferEmptyKeyPath];
303322
[_playerItem removeObserver:self forKeyPath:playbackLikelyToKeepUpKeyPath];
323+
[_playerItem removeObserver:self forKeyPath:loadedTimeRangesKeyPath];
304324
[_playerItem removeObserver:self forKeyPath:timedMetadata];
305325
_playerItemObserversSet = NO;
306326
}
@@ -329,13 +349,13 @@ - (void)setSrc:(NSDictionary *)source
329349
[_player removeObserver:self forKeyPath:playbackRate context:nil];
330350
_playbackRateObserverRegistered = NO;
331351
}
332-
352+
333353
_player = [AVPlayer playerWithPlayerItem:_playerItem];
334354
_player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
335355

336356
[_player addObserver:self forKeyPath:playbackRate options:0 context:nil];
337357
_playbackRateObserverRegistered = YES;
338-
358+
339359
[self addPlayerTimeObserver];
340360

341361
//Perform on next run loop, otherwise onVideoLoadStart is nil
@@ -621,6 +641,23 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
621641
}
622642
_playerBufferEmpty = NO;
623643
self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag});
644+
} else if ([keyPath isEqualToString:loadedTimeRangesKeyPath]) {
645+
if (_bufferConfig) {
646+
double buffered = [[self calculatePlayableDuration] doubleValue] - [[NSNumber numberWithFloat:CMTimeGetSeconds(_player.currentTime)] doubleValue];
647+
double threshold = 0.0;
648+
if (_bufferConfig[@"bufferForPlaybackAfterRebufferMs"]) {
649+
double playbackAfterRebufferMs = [_bufferConfig[@"bufferForPlaybackAfterRebufferMs"] doubleValue];
650+
threshold = playbackAfterRebufferMs / 1000; // default to playbackAfterRebufferMs
651+
}
652+
if ((!_playbackStarted || _seeked) && _bufferConfig[@"bufferForPlaybackMs"]) {
653+
// video is yet to start playback, or user has interrupted video with a seek event
654+
double bufferForPlaybackMs = [_bufferConfig[@"bufferForPlaybackMs"] doubleValue];
655+
threshold = bufferForPlaybackMs / 1000; // bufferForPlaybackMs
656+
}
657+
if (threshold > 0.0 && buffered >= threshold && !_paused) {
658+
[_player playImmediatelyAtRate:1];
659+
}
660+
}
624661
}
625662
} else if (object == _playerLayer) {
626663
if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey]) {
@@ -791,7 +828,7 @@ - (void)setSeek:(NSDictionary *)info
791828
@"target": self.reactTag});
792829
}
793830
}];
794-
831+
_seeked = YES;
795832
_pendingSeek = false;
796833
}
797834

@@ -830,6 +867,7 @@ - (void)applyModifiers
830867
[_player setMuted:NO];
831868
}
832869

870+
[self setBufferConfig:_bufferConfig];
833871
[self setSelectedAudioTrack:_selectedAudioTrack];
834872
[self setSelectedTextTrack:_selectedTextTrack];
835873
[self setResizeMode:_resizeMode];
@@ -888,6 +926,10 @@ - (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)character
888926
[_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group];
889927
}
890928

929+
- (void)setBufferConfig:(NSDictionary *)bufferConfig {
930+
_bufferConfig = bufferConfig;
931+
}
932+
891933
- (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack {
892934
_selectedAudioTrack = selectedAudioTrack;
893935
[self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible

ios/RCTVideoManager.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ - (dispatch_queue_t)methodQueue
3838
RCT_EXPORT_VIEW_PROPERTY(currentTime, float);
3939
RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL);
4040
RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
41+
RCT_EXPORT_VIEW_PROPERTY(bufferConfig, NSDictionary);
4142
/* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */
4243
RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock);
4344
RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTBubblingEventBlock);

0 commit comments

Comments
 (0)