From a829790da1d00c9699f303ee8a015f4e2dc42432 Mon Sep 17 00:00:00 2001 From: Armands Malejevs Date: Mon, 8 Nov 2021 16:56:48 +0200 Subject: [PATCH 1/3] Add support for content resolutions and track selection improvements --- .../exoplayer/ReactExoplayerView.java | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) 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 24dda940ff..419f3c6c2a 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -76,6 +76,14 @@ import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.source.dash.DashUtil; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.Period; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.manifest.Descriptor; +import com.google.android.exoplayer2.util.EventLogger; import java.net.CookieHandler; import java.net.CookieManager; @@ -86,6 +94,13 @@ import java.util.Map; import java.util.Timer; import java.util.TimerTask; +import java.util.List; +import java.lang.Thread; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.lang.Integer; @SuppressLint("ViewConstructor") class ReactExoplayerView extends FrameLayout implements @@ -136,6 +151,8 @@ class ReactExoplayerView extends FrameLayout implements private int maxBitRate = 0; private long seekTime = C.TIME_UNSET; private boolean hasDrmFailed = false; + private boolean isUsingContentResolution = false; + private boolean selectTrackWhenReady = false; private int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS; private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS; @@ -489,6 +506,7 @@ public void run() { .setBandwidthMeter(bandwidthMeter) .setLoadControl(loadControl) .build(); + player.addAnalyticsListener(new EventLogger(trackSelector)); player.addListener(self); player.addMetadataOutput(self); exoPlayerView.setPlayer(player); @@ -850,6 +868,10 @@ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { onBuffering(false); startProgressHandler(); videoLoaded(); + if (selectTrackWhenReady && isUsingContentResolution) { + selectTrackWhenReady = false; + setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); + } // Setting the visibility for the playerControlView if (playerControlView != null) { playerControlView.show(); @@ -921,6 +943,13 @@ private WritableArray getAudioTrackInfo() { return audioTracks; } private WritableArray getVideoTrackInfo() { + + WritableArray contentVideoTracks = this.getVideoTrackInfoFromManifest(); + if (contentVideoTracks != null) { + isUsingContentResolution = true; + return contentVideoTracks; + } + WritableArray videoTracks = Arguments.createArray(); MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); @@ -947,9 +976,71 @@ private WritableArray getVideoTrackInfo() { } } } + return videoTracks; } + private WritableArray getVideoTrackInfoFromManifest() { + ExecutorService es = Executors.newSingleThreadExecutor(); + final DataSource dataSource = this.mediaDataSourceFactory.createDataSource(); + final Uri sourceUri = this.srcUri; + final Timeline timelineRef = this.player.getCurrentTimeline(); + + Future result = es.submit(new Callable() { + DataSource ds = dataSource; + Uri uri = sourceUri; + Timeline timeline = timelineRef; + public WritableArray call() throws Exception { + WritableArray videoTracks = Arguments.createArray(); + try { + DashManifest manifest = DashUtil.loadManifest(this.ds, this.uri); + int periodCount = manifest.getPeriodCount(); + for (int i = 0; i < periodCount; i++) { + Period period = manifest.getPeriod(i); + for (int adaptationIndex = 0; adaptationIndex < period.adaptationSets.size(); adaptationIndex++) { + AdaptationSet adaptation = period.adaptationSets.get(adaptationIndex); + if (adaptation.type != C.TRACK_TYPE_VIDEO) { + continue; + } + boolean hasFoundContentPeriod = false; + for (int representationIndex = 0; representationIndex < adaptation.representations.size(); representationIndex++) { + Representation representation = adaptation.representations.get(representationIndex); + Format format = representation.format; + boolean hasDrm = format.drmInitData != null; + if (!hasDrm) { + break; + } + hasFoundContentPeriod = true; + 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(representationIndex) : format.id); + if (isFormatSupported(format)) { + videoTracks.pushMap(videoTrack); + } + } + if (hasFoundContentPeriod) { + return videoTracks; + } + } + } + } catch (Exception e) {} + return null; + } + }); + + try { + WritableArray results = result.get(); + es.shutdown(); + return results; + } catch (Exception e) {} + + return null; + } + private WritableArray getTextTrackInfo() { WritableArray textTracks = Arguments.createArray(); @@ -993,6 +1084,11 @@ public void onPositionDiscontinuity(int reason) { // which they seeked. updateResumePosition(); } + if (isUsingContentResolution) { + // Discontinuity events might have a different track list so we update the selected track + setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); + selectTrackWhenReady = true; + } // When repeat is turned on, reaching the end of the video will not cause a state change // so we need to explicitly detect it. if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION @@ -1010,6 +1106,10 @@ public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { public void onSeekProcessed() { eventEmitter.seek(player.getCurrentPosition(), seekTime); seekTime = C.TIME_UNSET; + if (isUsingContentResolution) { + // We need to update the selected track to make sure that it still matches user selection if track list has changed in this period + setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); + } } @Override @@ -1278,14 +1378,51 @@ public void setSelectedTrack(int trackType, String type, Dynamic value) { int height = value.asInt(); for (int i = 0; i < groups.length; ++i) { // Search for the exact height TrackGroup group = groups.get(i); + Format closestFormat = null; + int closestTrackIndex = -1; + boolean usingExactMatch = false; for (int j = 0; j < group.length; j++) { Format format = group.getFormat(j); if (format.height == height) { groupIndex = i; tracks[0] = j; + closestFormat = null; + closestTrackIndex = -1; + usingExactMatch = true; break; + } else if (isUsingContentResolution) { + // When using content resolution rather than ads, we need to try and find the closest match if there is no exact match + if (closestFormat != null) { + if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) && format.height < height) { + // Higher quality match + closestFormat = format; + closestTrackIndex = j; + } + } else if(format.height < height) { + closestFormat = format; + closestTrackIndex = j; + } + } + } + // This is a fallback if the new period contains only higher resolutions than the user has selected + if (closestFormat == null && isUsingContentResolution && !usingExactMatch) { + // No close match found - so we pick the lowest quality + int minHeight = Integer.MAX_VALUE; + for (int j = 0; j < group.length; j++) { + Format format = group.getFormat(j); + if (format.height < minHeight) { + minHeight = format.height; + groupIndex = i; + tracks[0] = j; + } } } + // Selecting the closest match found + if (closestFormat != null && closestTrackIndex != -1) { + // We found the closest match instead of an exact one + groupIndex = i; + tracks[0] = closestTrackIndex; + } } } else if (rendererIndex == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default // Use system settings if possible From ce590fda0f1a1c07197cd4b40a1de5e5b77d2494 Mon Sep 17 00:00:00 2001 From: Armands Malejevs Date: Tue, 9 Nov 2021 14:17:46 +0200 Subject: [PATCH 2/3] Improve content representation detection --- Video.js | 1 + .../com/brentvatne/exoplayer/ReactExoplayerView.java | 11 +++++++++-- .../exoplayer/ReactExoplayerViewManager.java | 6 ++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Video.js b/Video.js index 62a179a445..fdfa141ec8 100644 --- a/Video.js +++ b/Video.js @@ -477,6 +477,7 @@ Video.propTypes = { playWhenInactive: PropTypes.bool, ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']), reportBandwidth: PropTypes.bool, + contentStartTime: PropTypes.number, disableFocus: PropTypes.bool, disableBuffering: PropTypes.bool, controls: 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 419f3c6c2a..135632467f 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -176,6 +176,7 @@ class ReactExoplayerView extends FrameLayout implements private ReadableArray textTracks; private boolean disableFocus; private boolean disableBuffering; + private long contentStartTime; private boolean disableDisconnectError; private boolean preventsDisplaySleepDuringVideoPlayback = true; private float mProgressUpdateInterval = 250.0f; @@ -985,11 +986,14 @@ private WritableArray getVideoTrackInfoFromManifest() { final DataSource dataSource = this.mediaDataSourceFactory.createDataSource(); final Uri sourceUri = this.srcUri; final Timeline timelineRef = this.player.getCurrentTimeline(); + final long startTime = this.contentStartTime * 1000 - 100; // s -> ms with 100ms offset Future result = es.submit(new Callable() { DataSource ds = dataSource; Uri uri = sourceUri; Timeline timeline = timelineRef; + long startTimeUs = startTime * 1000; // ms -> us + public WritableArray call() throws Exception { WritableArray videoTracks = Arguments.createArray(); try { @@ -1006,8 +1010,7 @@ public WritableArray call() throws Exception { for (int representationIndex = 0; representationIndex < adaptation.representations.size(); representationIndex++) { Representation representation = adaptation.representations.get(representationIndex); Format format = representation.format; - boolean hasDrm = format.drmInitData != null; - if (!hasDrm) { + if (representation.presentationTimeOffsetUs <= startTimeUs) { break; } hasFoundContentPeriod = true; @@ -1605,6 +1608,10 @@ public void setBackBufferDurationMs(int backBufferDurationMs) { this.backBufferDurationMs = backBufferDurationMs; } + public void setContentStartTime(int contentStartTime) { + this.contentStartTime = (long)contentStartTime; + } + public void setDisableBuffering(boolean disableBuffering) { this.disableBuffering = disableBuffering; } 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 fc9a1354f2..0e7a8c66cd 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -63,6 +63,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager Date: Tue, 9 Nov 2021 14:20:35 +0200 Subject: [PATCH 3/3] Remove logging --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 2 -- 1 file changed, 2 deletions(-) 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 135632467f..7873d95d20 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -83,7 +83,6 @@ import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.source.dash.manifest.Descriptor; -import com.google.android.exoplayer2.util.EventLogger; import java.net.CookieHandler; import java.net.CookieManager; @@ -507,7 +506,6 @@ public void run() { .setBandwidthMeter(bandwidthMeter) .setLoadControl(loadControl) .build(); - player.addAnalyticsListener(new EventLogger(trackSelector)); player.addListener(self); player.addMetadataOutput(self); exoPlayerView.setPlayer(player);