diff --git a/Video.js b/Video.js index e726f401..92544467 100644 --- a/Video.js +++ b/Video.js @@ -106,6 +106,12 @@ export default class Video extends Component { } }; + _onBandwidthUpdate = (event) => { + if (this.props.onBandwidthUpdate) { + this.props.onBandwidthUpdate(event.nativeEvent); + } + }; + _onSeek = (event) => { if (this.state.showPoster && !this.props.audioOnly) { this.setState({showPoster: false}); @@ -247,6 +253,7 @@ export default class Video extends Component { onVideoSeek: this._onSeek, onVideoEnd: this._onEnd, onVideoBuffer: this._onBuffer, + onVideoBandwidthUpdate: this._onBandwidthUpdate, onTimedMetadata: this._onTimedMetadata, onVideoAudioBecomingNoisy: this._onAudioBecomingNoisy, onVideoExternalPlaybackChange: this._onExternalPlaybackChange, @@ -313,6 +320,7 @@ Video.propTypes = { onVideoBuffer: PropTypes.func, onVideoError: PropTypes.func, onVideoProgress: PropTypes.func, + onVideoBandwidthUpdate: PropTypes.func, onVideoSeek: PropTypes.func, onVideoEnd: PropTypes.func, onTimedMetadata: PropTypes.func, @@ -344,6 +352,13 @@ Video.propTypes = { PropTypes.number ]) }), + selectedVideoTrack: PropTypes.shape({ + type: PropTypes.string.isRequired, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ]) + }), selectedTextTrack: PropTypes.shape({ type: PropTypes.string.isRequired, value: PropTypes.oneOfType([ @@ -377,6 +392,7 @@ Video.propTypes = { playInBackground: PropTypes.bool, playWhenInactive: PropTypes.bool, ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']), + reportBandwidth: PropTypes.bool, disableFocus: PropTypes.bool, controls: PropTypes.bool, audioOnly: PropTypes.bool, @@ -391,6 +407,7 @@ Video.propTypes = { onBuffer: PropTypes.func, onError: PropTypes.func, onProgress: PropTypes.func, + onBandwidthUpdate: PropTypes.func, onSeek: PropTypes.func, onEnd: PropTypes.func, onFullscreenPlayerWillPresent: PropTypes.func, diff --git a/android-exoplayer/build.gradle b/android-exoplayer/build.gradle index a532956d..614d010c 100644 --- a/android-exoplayer/build.gradle +++ b/android-exoplayer/build.gradle @@ -21,6 +21,12 @@ dependencies { implementation('com.google.android.exoplayer:exoplayer:2.9.3') { exclude group: 'com.android.support' } + implementation project(':exoplayer-library-core') + implementation project(':exoplayer-library-dash') + implementation project(':exoplayer-library-ui') + implementation project(':exoplayer-library-smoothstreaming') + implementation project(':exoplayer-library-hls') + implementation project(':exoplayer-extension-okhttp') // All support libs must use the same version implementation "com.android.support:support-annotations:${safeExtGet('supportLibVersion', '+')}" 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 602c6df7..27aaf3b7 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -46,6 +46,7 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; @@ -59,6 +60,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultAllocator; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -76,6 +78,7 @@ import java.util.Locale; class ReactExoplayerView extends FrameLayout implements LifecycleEventListener, ExoPlayer.EventListener, + BandwidthMeter.EventListener, BecomingNoisyListener, AudioManager.OnAudioFocusChangeListener, MetadataRenderer.Output { @@ -85,6 +88,7 @@ class ReactExoplayerView extends FrameLayout implements private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final CookieManager DEFAULT_COOKIE_MANAGER; private static final int SHOW_PROGRESS = 1; + private static final int REPORT_BANDWIDTH = 1; static { DEFAULT_COOKIE_MANAGER = new CookieManager(); @@ -92,7 +96,7 @@ class ReactExoplayerView extends FrameLayout implements } private final VideoEventEmitter eventEmitter; - + private Handler mainHandler; private ExoPlayerView exoPlayerView; @@ -124,6 +128,8 @@ class ReactExoplayerView extends FrameLayout implements private boolean repeat; private String audioTrackType; private Dynamic audioTrackValue; + private String videoTrackType; + private Dynamic videoTrackValue; private ReadableArray audioTracks; private String textTrackType; private Dynamic textTrackValue; @@ -132,6 +138,7 @@ class ReactExoplayerView extends FrameLayout implements private float mProgressUpdateInterval = 250.0f; private boolean playInBackground = false; private Map requestHeaders; + private boolean mReportBandwidth = false; // \ End props // React @@ -162,8 +169,11 @@ class ReactExoplayerView extends FrameLayout implements public ReactExoplayerView(ThemedReactContext context) { super(context); this.themedReactContext = context; - createViews(); + this.eventEmitter = new VideoEventEmitter(context); + + createViews(); + audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); themedReactContext.addLifecycleEventListener(this); audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext); @@ -238,9 +248,15 @@ class ReactExoplayerView extends FrameLayout implements stopPlayback(); } + //BandwidthMeter.EventListener implementation + @Override + public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) { + if (mReportBandwidth) { + eventEmitter.bandwidthReport(bitrate); + } + } // Internal methods - private void initializePlayer() { if (player == null) { TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); @@ -255,6 +271,7 @@ class ReactExoplayerView extends FrameLayout implements player.setMetadataOutput(this); exoPlayerView.setPlayer(player); audioBecomingNoisyReceiver.setListener(this); + BANDWIDTH_METER.addEventListener(new Handler(), this); setPlayWhenReady(!isPaused); playerNeedsSource = true; @@ -345,6 +362,7 @@ class ReactExoplayerView extends FrameLayout implements progressHandler.removeMessages(SHOW_PROGRESS); themedReactContext.removeLifecycleEventListener(this); audioBecomingNoisyReceiver.removeListener(); + BANDWIDTH_METER.removeEventListener(this); } private boolean requestAudioFocus() { @@ -520,12 +538,13 @@ class ReactExoplayerView extends FrameLayout implements if (loadVideoStarted) { loadVideoStarted = false; setSelectedAudioTrack(audioTrackType, audioTrackValue); + setSelectedVideoTrack(videoTrackType, videoTrackValue); setSelectedTextTrack(textTrackType, textTrackValue); Format videoFormat = player.getVideoFormat(); int width = videoFormat != null ? videoFormat.width : 0; int height = videoFormat != null ? videoFormat.height : 0; eventEmitter.load(player.getDuration(), player.getCurrentPosition(), width, height, - getAudioTrackInfo(), getTextTrackInfo()); + getAudioTrackInfo(), getTextTrackInfo(), getVideoTrackInfo()); } } @@ -546,10 +565,39 @@ class ReactExoplayerView extends FrameLayout implements audioTrack.putString("title", format.id != null ? format.id : ""); audioTrack.putString("type", format.sampleMimeType); audioTrack.putString("language", format.language != null ? format.language : ""); + audioTrack.putString("bitrate", format.bitrate == Format.NO_VALUE ? "" + : String.format(Locale.US, "%.2fMbps", format.bitrate / 1000000f)); audioTracks.pushMap(audioTrack); } return audioTracks; } + private WritableArray getVideoTrackInfo() { + WritableArray videoTracks = Arguments.createArray(); + + MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); + int index = getTrackRendererIndex(C.TRACK_TYPE_VIDEO); + if (info == null || index == C.INDEX_UNSET) { + return videoTracks; + } + + TrackGroupArray groups = info.getTrackGroups(index); + for (int i = 0; i < groups.length; ++i) { + TrackGroup group = groups.get(i); + + for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { + Format format = group.getFormat(trackIndex); + WritableMap videoTrack = Arguments.createMap(); + videoTrack.putInt("width", format.width == Format.NO_VALUE ? 0 : format.width); + videoTrack.putInt("height",format.height == Format.NO_VALUE ? 0 : format.height); + videoTrack.putInt("bitrate", format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); + videoTrack.putString("codecs", format.codecs != null ? format.codecs : ""); + videoTrack.putString("trackId", + format.id == null ? String.valueOf(trackIndex) : format.id); + videoTracks.pushMap(videoTrack); + } + } + return videoTracks; + } private WritableArray getTextTrackInfo() { WritableArray textTracks = Arguments.createArray(); @@ -726,6 +774,10 @@ class ReactExoplayerView extends FrameLayout implements mProgressUpdateInterval = progressUpdateInterval; } + public void setReportBandwidth(boolean reportBandwidth) { + mReportBandwidth = reportBandwidth; + } + public void setRawSrc(final Uri uri, final String extension) { if (uri != null) { boolean isOriginalSourceNull = srcUri == null; @@ -777,7 +829,8 @@ class ReactExoplayerView extends FrameLayout implements } TrackGroupArray groups = info.getTrackGroups(rendererIndex); - int trackIndex = C.INDEX_UNSET; + int groupIndex = C.INDEX_UNSET; + int[] tracks = {0} ; if (TextUtils.isEmpty(type)) { type = "default"; @@ -795,7 +848,7 @@ class ReactExoplayerView extends FrameLayout implements for (int i = 0; i < groups.length; ++i) { Format format = groups.get(i).getFormat(0); if (format.language != null && format.language.equals(value.asString())) { - trackIndex = i; + groupIndex = i; break; } } @@ -803,28 +856,46 @@ class ReactExoplayerView extends FrameLayout implements for (int i = 0; i < groups.length; ++i) { Format format = groups.get(i).getFormat(0); if (format.id != null && format.id.equals(value.asString())) { - trackIndex = i; + groupIndex = i; break; } } } else if (type.equals("index")) { if (value.asInt() < groups.length) { - trackIndex = value.asInt(); + groupIndex = value.asInt(); } - } else { // default - if (rendererIndex == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18 && groups.length > 0) { - // Use system settings if possible - CaptioningManager captioningManager - = (CaptioningManager)themedReactContext.getSystemService(Context.CAPTIONING_SERVICE); - if (captioningManager != null && captioningManager.isEnabled()) { - trackIndex = getTrackIndexForDefaultLocale(groups); + } else if (type.equals("resolution")) { + int height = value.asInt(); + for (int i = 0; i < groups.length; ++i) { // Search for the exact height + TrackGroup group = groups.get(i); + for (int j = 0; j < group.length; j++) { + Format format = group.getFormat(j); + if (format.height == value.asInt()) { + groupIndex = i; + tracks[0] = j; + break; + } } - } else if (rendererIndex == C.TRACK_TYPE_AUDIO) { - trackIndex = getTrackIndexForDefaultLocale(groups); } + } else if (rendererIndex == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default + // Use system settings if possible + CaptioningManager captioningManager + = (CaptioningManager)themedReactContext.getSystemService(Context.CAPTIONING_SERVICE); + if (captioningManager != null && captioningManager.isEnabled()) { + groupIndex = getGroupIndexForDefaultLocale(groups); + } + } else if (rendererIndex == C.TRACK_TYPE_AUDIO) { // Audio default + groupIndex = getGroupIndexForDefaultLocale(groups); } - if (trackIndex == C.INDEX_UNSET) { + if (groupIndex == C.INDEX_UNSET && trackType == C.TRACK_TYPE_VIDEO) { // Video auto + TrackGroup group = groups.get(0); + tracks = new int[group.length]; + groupIndex = 0; + for (int j = 0; j < group.length; j++) { + tracks[j] = j; + } + } else if (groupIndex == C.INDEX_UNSET) { trackSelector.setParameters(disableParameters); return; } @@ -833,28 +904,34 @@ class ReactExoplayerView extends FrameLayout implements .buildUpon() .setRendererDisabled(rendererIndex, false) .setSelectionOverride(rendererIndex, groups, - new DefaultTrackSelector.SelectionOverride(trackIndex, 0)) + new DefaultTrackSelector.SelectionOverride(groupIndex, tracks)) .build(); trackSelector.setParameters(selectionParameters); } - private int getTrackIndexForDefaultLocale(TrackGroupArray groups) { - if (groups.length == 0) { // Avoid a crash if we try to select a non-existant group + private int getGroupIndexForDefaultLocale(TrackGroupArray groups) { + if (groups.length == 0){ return C.INDEX_UNSET; } - int trackIndex = 0; // default if no match + int groupIndex = 0; // default if no match String locale2 = Locale.getDefault().getLanguage(); // 2 letter code String locale3 = Locale.getDefault().getISO3Language(); // 3 letter code for (int i = 0; i < groups.length; ++i) { Format format = groups.get(i).getFormat(0); String language = format.language; if (language != null && (language.equals(locale2) || language.equals(locale3))) { - trackIndex = i; + groupIndex = i; break; } } - return trackIndex; + return groupIndex; + } + + public void setSelectedVideoTrack(String type, Dynamic value) { + videoTrackType = type; + videoTrackValue = value; + setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); } public void setSelectedAudioTrack(String type, Dynamic value) { diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 84cc620a..31bd8e08 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -45,6 +45,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager> createJSModules() { - return Collections.emptyList(); - } + // Deprecated RN 0.47 + public List> createJSModules() { + return Collections.emptyList(); + } + @Override public List createViewManagers(ReactApplicationContext reactContext) { diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index eee5bca2..05527a57 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -24,6 +24,7 @@ @property (nonatomic, copy) RCTBubblingEventBlock onVideoBuffer; @property (nonatomic, copy) RCTBubblingEventBlock onVideoError; @property (nonatomic, copy) RCTBubblingEventBlock onVideoProgress; +@property (nonatomic, copy) RCTBubblingEventBlock onBandwidthUpdate; @property (nonatomic, copy) RCTBubblingEventBlock onVideoSeek; @property (nonatomic, copy) RCTBubblingEventBlock onVideoEnd; @property (nonatomic, copy) RCTBubblingEventBlock onTimedMetadata; diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 9c7e8b79..b71bbf28 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -704,6 +704,24 @@ static int const RCTVideoUnset = -1; selector:@selector(playbackStalled:) name:AVPlayerItemPlaybackStalledNotification object:nil]; + + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVPlayerItemNewAccessLogEntryNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleAVPlayerAccess:) + name:AVPlayerItemNewAccessLogEntryNotification + object:nil]; + +} + +- (void)handleAVPlayerAccess:(NSNotification *)notification { + AVPlayerItemAccessLog *accessLog = [((AVPlayerItem *)notification.object) accessLog]; + AVPlayerItemAccessLogEvent *lastEvent = accessLog.events.lastObject; + + if (self.onBandwidthUpdate) { + self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]}); + } } - (void)playbackStalled:(NSNotification *)notification diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index f7e6666a..1ca1b5b4 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -48,6 +48,7 @@ RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoBuffer, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoError, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoProgress, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onBandwidthUpdate, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoSeek, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoEnd, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onTimedMetadata, RCTBubblingEventBlock);