diff --git a/CHANGELOG.md b/CHANGELOG.md index 8033d1c6..880b695c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ - Fix video dimensions being undefined when playing HLS in ios. [#1992](https://github.com/react-native-community/react-native-video/pull/1992) - Add support for audio mix with other apps for iOS. [#1978](https://github.com/react-native-community/react-native-video/pull/1978) - Properly implement pending seek for iOS. [#1994](https://github.com/react-native-community/react-native-video/pull/1994) +- Added `preferredForwardBufferDuration` (iOS) - the duration the player should buffer media from the network ahead of the playhead to guard against playback disruption. (#1944) +- Added `currentPlaybackTime` (Android ExoPlayer, iOS) - 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. (#1944) +- Added `trackId` (Android ExoPlayer) - Configure an identifier for the video stream to link the playback context to the events emitted. (#1944) ### Version 5.1.0-alpha5 diff --git a/README.md b/README.md index 1b5e9d9c..9da556eb 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,7 @@ var styles = StyleSheet.create({ * [automaticallyWaitsToMinimizeStalling](#automaticallyWaitsToMinimizeStalling) * [bufferConfig](#bufferconfig) * [controls](#controls) +* [currentPlaybackTime](#currentPlaybackTime) * [disableFocus](#disableFocus) * [filter](#filter) * [filterEnabled](#filterEnabled) @@ -297,6 +298,7 @@ var styles = StyleSheet.create({ * [playWhenInactive](#playwheninactive) * [poster](#poster) * [posterResizeMode](#posterresizemode) +* [preferredForwardBufferDuration](#preferredForwardBufferDuration) * [progressUpdateInterval](#progressupdateinterval) * [rate](#rate) * [repeat](#repeat) @@ -308,6 +310,7 @@ var styles = StyleSheet.create({ * [source](#source) * [stereoPan](#stereopan) * [textTracks](#texttracks) +* [trackId](#trackId) * [useTextureView](#usetextureview) * [volume](#volume) @@ -386,6 +389,11 @@ bufferConfig={{ Platforms: Android ExoPlayer +#### 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. + +Platforms: Android ExoPlayer, iOS + #### controls Determines whether to show player controls. * ** false (default)** - Don't show player controls @@ -596,6 +604,13 @@ Determines how to resize the poster image when the frame doesn't match the raw v Platforms: all +#### preferredForwardBufferDuration +The duration the player should buffer media from the network ahead of the playhead to guard against playback disruption. Sets the [preferredForwardBufferDuration](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration) instance property on AVPlayerItem. + +Default: 0 + +Platforms: iOS + #### progressUpdateInterval Delay in milliseconds between onProgress events in milliseconds. @@ -831,6 +846,11 @@ textTracks={[ Platforms: Android ExoPlayer, iOS +#### trackId +Configure an identifier for the video stream to link the playback context to the events emitted. + +Platforms: Android ExoPlayer + #### useTextureView Controls whether to output to a TextureView or SurfaceView. diff --git a/Video.js b/Video.js index e6a313aa..06813173 100644 --- a/Video.js +++ b/Video.js @@ -479,6 +479,7 @@ Video.propTypes = { rate: PropTypes.number, pictureInPicture: PropTypes.bool, playInBackground: PropTypes.bool, + preferredForwardBufferDuration: PropTypes.number, playWhenInactive: PropTypes.bool, ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']), reportBandwidth: PropTypes.bool, diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index e6dcc186..1c487ae6 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -64,6 +64,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; import java.net.CookieHandler; @@ -161,7 +162,7 @@ class ReactExoplayerView extends FrameLayout implements ) { long pos = player.getCurrentPosition(); long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100; - eventEmitter.progressChanged(pos, bufferedDuration, player.getDuration()); + eventEmitter.progressChanged(pos, bufferedDuration, player.getDuration(), getPositionInFirstPeriodMsForCurrentWindow(pos)); msg = obtainMessage(SHOW_PROGRESS); sendMessageDelayed(msg, Math.round(mProgressUpdateInterval)); } @@ -169,6 +170,14 @@ class ReactExoplayerView extends FrameLayout implements } } }; + + public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) { + Timeline.Window window = new Timeline.Window(); + if(!player.getCurrentTimeline().isEmpty()) { + player.getCurrentTimeline().getWindow(player.getCurrentWindowIndex(), window); + } + return window.windowStartTimeMs + currentPosition; + } public ReactExoplayerView(ThemedReactContext context, ReactExoplayerConfig config) { super(context); @@ -257,7 +266,15 @@ class ReactExoplayerView extends FrameLayout implements @Override public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) { if (mReportBandwidth) { - eventEmitter.bandwidthReport(bitrate); + if (player == null) { + eventEmitter.bandwidthReport(bitrate, 0, 0, "-1"); + } else { + Format videoFormat = player.getVideoFormat(); + int width = videoFormat != null ? videoFormat.width : 0; + int height = videoFormat != null ? videoFormat.height : 0; + String trackId = videoFormat != null ? videoFormat.id : "-1"; + eventEmitter.bandwidthReport(bitrate, height, width, trackId); + } } } @@ -749,8 +766,9 @@ class ReactExoplayerView extends FrameLayout implements Format videoFormat = player.getVideoFormat(); int width = videoFormat != null ? videoFormat.width : 0; int height = videoFormat != null ? videoFormat.height : 0; + String trackId = videoFormat != null ? videoFormat.id : "-1"; eventEmitter.load(player.getDuration(), player.getCurrentPosition(), width, height, - getAudioTrackInfo(), getTextTrackInfo(), getVideoTrackInfo()); + getAudioTrackInfo(), getTextTrackInfo(), getVideoTrackInfo(), trackId); } } @@ -889,7 +907,7 @@ class ReactExoplayerView extends FrameLayout implements @Override public void onPlayerError(ExoPlaybackException e) { - String errorString = null; + String errorString = "ExoPlaybackException type : " + e.type; Exception ex = e; if (e.type == ExoPlaybackException.TYPE_RENDERER) { Exception cause = e.getRendererException(); @@ -914,12 +932,9 @@ class ReactExoplayerView extends FrameLayout implements } } else if (e.type == ExoPlaybackException.TYPE_SOURCE) { - ex = e.getSourceException(); errorString = getResources().getString(R.string.unrecognized_media_format); } - if (errorString != null) { - eventEmitter.error(errorString, ex); - } + eventEmitter.error(errorString, ex); playerNeedsSource = true; if (isBehindLiveWindow(e)) { clearResumePosition(); @@ -930,12 +945,14 @@ class ReactExoplayerView extends FrameLayout implements } private static boolean isBehindLiveWindow(ExoPlaybackException e) { + Log.e("ExoPlayer Exception", e.toString()); if (e.type != ExoPlaybackException.TYPE_SOURCE) { return false; } Throwable cause = e.getSourceException(); while (cause != null) { - if (cause instanceof BehindLiveWindowException) { + if (cause instanceof BehindLiveWindowException || + cause instanceof HttpDataSource.HttpDataSourceException) { return true; } cause = cause.getCause(); diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java index 24d51e02..7a60096a 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java @@ -107,8 +107,10 @@ class VideoEventEmitter { private static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration"; private static final String EVENT_PROP_SEEKABLE_DURATION = "seekableDuration"; private static final String EVENT_PROP_CURRENT_TIME = "currentTime"; + private static final String EVENT_PROP_CURRENT_PLAYBACK_TIME = "currentPlaybackTime"; private static final String EVENT_PROP_SEEK_TIME = "seekTime"; private static final String EVENT_PROP_NATURAL_SIZE = "naturalSize"; + private static final String EVENT_PROP_TRACK_ID = "trackId"; private static final String EVENT_PROP_WIDTH = "width"; private static final String EVENT_PROP_HEIGHT = "height"; private static final String EVENT_PROP_ORIENTATION = "orientation"; @@ -137,7 +139,7 @@ class VideoEventEmitter { } void load(double duration, double currentPosition, int videoWidth, int videoHeight, - WritableArray audioTracks, WritableArray textTracks, WritableArray videoTracks) { + WritableArray audioTracks, WritableArray textTracks, WritableArray videoTracks, String trackId) { WritableMap event = Arguments.createMap(); event.putDouble(EVENT_PROP_DURATION, duration / 1000D); event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D); @@ -151,7 +153,7 @@ class VideoEventEmitter { naturalSize.putString(EVENT_PROP_ORIENTATION, "portrait"); } event.putMap(EVENT_PROP_NATURAL_SIZE, naturalSize); - + event.putString(EVENT_PROP_TRACK_ID, trackId); event.putArray(EVENT_PROP_VIDEO_TRACKS, videoTracks); event.putArray(EVENT_PROP_AUDIO_TRACKS, audioTracks); event.putArray(EVENT_PROP_TEXT_TRACKS, textTracks); @@ -168,17 +170,21 @@ class VideoEventEmitter { receiveEvent(EVENT_LOAD, event); } - void progressChanged(double currentPosition, double bufferedDuration, double seekableDuration) { + void progressChanged(double currentPosition, double bufferedDuration, double seekableDuration, double currentPlaybackTime) { WritableMap event = Arguments.createMap(); event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D); event.putDouble(EVENT_PROP_PLAYABLE_DURATION, bufferedDuration / 1000D); event.putDouble(EVENT_PROP_SEEKABLE_DURATION, seekableDuration / 1000D); + event.putDouble(EVENT_PROP_CURRENT_PLAYBACK_TIME, currentPlaybackTime); receiveEvent(EVENT_PROGRESS, event); } - void bandwidthReport(double bitRateEstimate) { + void bandwidthReport(double bitRateEstimate, int height, int width, String id) { WritableMap event = Arguments.createMap(); event.putDouble(EVENT_PROP_BITRATE, bitRateEstimate); + event.putInt(EVENT_PROP_WIDTH, width); + event.putInt(EVENT_PROP_HEIGHT, height); + event.putString(EVENT_PROP_TRACK_ID, id); receiveEvent(EVENT_BANDWIDTH, event); } @@ -226,7 +232,7 @@ class VideoEventEmitter { void error(String errorString, Exception exception) { WritableMap error = Arguments.createMap(); error.putString(EVENT_PROP_ERROR_STRING, errorString); - error.putString(EVENT_PROP_ERROR_EXCEPTION, exception.getMessage()); + error.putString(EVENT_PROP_ERROR_EXCEPTION, exception.toString()); WritableMap event = Arguments.createMap(); event.putMap(EVENT_PROP_ERROR, error); receiveEvent(EVENT_ERROR, event); diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index d20913d2..8bfe702b 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -64,6 +64,7 @@ static int const RCTVideoUnset = -1; NSDictionary * _selectedAudioTrack; BOOL _playbackStalled; BOOL _playInBackground; + float _preferredForwardBufferDuration; BOOL _playWhenInactive; BOOL _pictureInPicture; NSString * _ignoreSilentSwitch; @@ -105,6 +106,7 @@ static int const RCTVideoUnset = -1; _controls = NO; _playerBufferEmpty = YES; _playInBackground = false; + _preferredForwardBufferDuration = 0.0f; _allowsExternalPlayback = YES; _playWhenInactive = false; _pictureInPicture = false; @@ -265,6 +267,7 @@ static int const RCTVideoUnset = -1; } CMTime currentTime = _player.currentTime; + NSDate *currentPlaybackTime = _player.currentItem.currentDate; const Float64 duration = CMTimeGetSeconds(playerDuration); const Float64 currentTimeSecs = CMTimeGetSeconds(currentTime); @@ -276,6 +279,7 @@ static int const RCTVideoUnset = -1; @"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], }); @@ -354,6 +358,7 @@ static int const RCTVideoUnset = -1; // perform on next run loop, otherwise other passed react-props may not be set [self playerItemForSource:source withCallback:^(AVPlayerItem * playerItem) { _playerItem = playerItem; + [self setPreferredForwardBufferDuration:_preferredForwardBufferDuration]; [self addPlayerItemObservers]; [self setFilter:_filterName]; [self setMaxBitRate:_maxBitRate]; @@ -995,6 +1000,12 @@ static int const RCTVideoUnset = -1; _playerItem.preferredPeakBitRate = maxBitRate; } +- (void)setPreferredForwardBufferDuration:(float) preferredForwardBufferDuration +{ + _preferredForwardBufferDuration = preferredForwardBufferDuration; + _playerItem.preferredForwardBufferDuration = preferredForwardBufferDuration; +} + - (void)setAutomaticallyWaitsToMinimizeStalling:(BOOL)waits { _automaticallyWaitsToMinimizeStalling = waits; diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index e8bda250..3059ed79 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -32,6 +32,7 @@ RCT_EXPORT_VIEW_PROPERTY(muted, BOOL); RCT_EXPORT_VIEW_PROPERTY(controls, BOOL); RCT_EXPORT_VIEW_PROPERTY(volume, float); RCT_EXPORT_VIEW_PROPERTY(playInBackground, BOOL); +RCT_EXPORT_VIEW_PROPERTY(preferredForwardBufferDuration, float); RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL); RCT_EXPORT_VIEW_PROPERTY(pictureInPicture, BOOL); RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString);