VEX-6011: Align AVOD resolutions with available resolutions on Content (#14)

Add support for content tracks and improve track selection to work even during ads playback.
This commit is contained in:
Armands Malejev 2021-11-09 14:22:32 +02:00 committed by GitHub
parent e27baeb065
commit f712eecb4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 149 additions and 0 deletions

View File

@ -477,6 +477,7 @@ Video.propTypes = {
playWhenInactive: PropTypes.bool, playWhenInactive: PropTypes.bool,
ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']), ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']),
reportBandwidth: PropTypes.bool, reportBandwidth: PropTypes.bool,
contentStartTime: PropTypes.number,
disableFocus: PropTypes.bool, disableFocus: PropTypes.bool,
disableBuffering: PropTypes.bool, disableBuffering: PropTypes.bool,
controls: PropTypes.bool, controls: PropTypes.bool,

View File

@ -76,6 +76,13 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; 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 java.net.CookieHandler; import java.net.CookieHandler;
import java.net.CookieManager; import java.net.CookieManager;
@ -86,6 +93,13 @@ import java.util.UUID;
import java.util.Map; import java.util.Map;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; 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") @SuppressLint("ViewConstructor")
class ReactExoplayerView extends FrameLayout implements class ReactExoplayerView extends FrameLayout implements
@ -136,6 +150,8 @@ class ReactExoplayerView extends FrameLayout implements
private int maxBitRate = 0; private int maxBitRate = 0;
private long seekTime = C.TIME_UNSET; private long seekTime = C.TIME_UNSET;
private boolean hasDrmFailed = false; private boolean hasDrmFailed = false;
private boolean isUsingContentResolution = false;
private boolean selectTrackWhenReady = false;
private int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS; private int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS; private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
@ -159,6 +175,7 @@ class ReactExoplayerView extends FrameLayout implements
private ReadableArray textTracks; private ReadableArray textTracks;
private boolean disableFocus; private boolean disableFocus;
private boolean disableBuffering; private boolean disableBuffering;
private long contentStartTime;
private boolean disableDisconnectError; private boolean disableDisconnectError;
private boolean preventsDisplaySleepDuringVideoPlayback = true; private boolean preventsDisplaySleepDuringVideoPlayback = true;
private float mProgressUpdateInterval = 250.0f; private float mProgressUpdateInterval = 250.0f;
@ -850,6 +867,10 @@ class ReactExoplayerView extends FrameLayout implements
onBuffering(false); onBuffering(false);
startProgressHandler(); startProgressHandler();
videoLoaded(); videoLoaded();
if (selectTrackWhenReady && isUsingContentResolution) {
selectTrackWhenReady = false;
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
}
// Setting the visibility for the playerControlView // Setting the visibility for the playerControlView
if (playerControlView != null) { if (playerControlView != null) {
playerControlView.show(); playerControlView.show();
@ -921,6 +942,13 @@ class ReactExoplayerView extends FrameLayout implements
return audioTracks; return audioTracks;
} }
private WritableArray getVideoTrackInfo() { private WritableArray getVideoTrackInfo() {
WritableArray contentVideoTracks = this.getVideoTrackInfoFromManifest();
if (contentVideoTracks != null) {
isUsingContentResolution = true;
return contentVideoTracks;
}
WritableArray videoTracks = Arguments.createArray(); WritableArray videoTracks = Arguments.createArray();
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
@ -947,9 +975,73 @@ class ReactExoplayerView extends FrameLayout implements
} }
} }
} }
return videoTracks; 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();
final long startTime = this.contentStartTime * 1000 - 100; // s -> ms with 100ms offset
Future<WritableArray> result = es.submit(new Callable<WritableArray>() {
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 {
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;
if (representation.presentationTimeOffsetUs <= startTimeUs) {
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() { private WritableArray getTextTrackInfo() {
WritableArray textTracks = Arguments.createArray(); WritableArray textTracks = Arguments.createArray();
@ -993,6 +1085,11 @@ class ReactExoplayerView extends FrameLayout implements
// which they seeked. // which they seeked.
updateResumePosition(); 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 // When repeat is turned on, reaching the end of the video will not cause a state change
// so we need to explicitly detect it. // so we need to explicitly detect it.
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
@ -1010,6 +1107,10 @@ class ReactExoplayerView extends FrameLayout implements
public void onSeekProcessed() { public void onSeekProcessed() {
eventEmitter.seek(player.getCurrentPosition(), seekTime); eventEmitter.seek(player.getCurrentPosition(), seekTime);
seekTime = C.TIME_UNSET; 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 @Override
@ -1278,13 +1379,50 @@ class ReactExoplayerView extends FrameLayout implements
int height = value.asInt(); int height = value.asInt();
for (int i = 0; i < groups.length; ++i) { // Search for the exact height for (int i = 0; i < groups.length; ++i) { // Search for the exact height
TrackGroup group = groups.get(i); TrackGroup group = groups.get(i);
Format closestFormat = null;
int closestTrackIndex = -1;
boolean usingExactMatch = false;
for (int j = 0; j < group.length; j++) { for (int j = 0; j < group.length; j++) {
Format format = group.getFormat(j); Format format = group.getFormat(j);
if (format.height == height) { if (format.height == height) {
groupIndex = i; groupIndex = i;
tracks[0] = j; tracks[0] = j;
closestFormat = null;
closestTrackIndex = -1;
usingExactMatch = true;
break; 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 } else if (rendererIndex == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default
@ -1468,6 +1606,10 @@ class ReactExoplayerView extends FrameLayout implements
this.backBufferDurationMs = backBufferDurationMs; this.backBufferDurationMs = backBufferDurationMs;
} }
public void setContentStartTime(int contentStartTime) {
this.contentStartTime = (long)contentStartTime;
}
public void setDisableBuffering(boolean disableBuffering) { public void setDisableBuffering(boolean disableBuffering) {
this.disableBuffering = disableBuffering; this.disableBuffering = disableBuffering;
} }

View File

@ -63,6 +63,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount"; private static final String PROP_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount";
private static final String PROP_MAXIMUM_BIT_RATE = "maxBitRate"; private static final String PROP_MAXIMUM_BIT_RATE = "maxBitRate";
private static final String PROP_PLAY_IN_BACKGROUND = "playInBackground"; private static final String PROP_PLAY_IN_BACKGROUND = "playInBackground";
private static final String PROP_CONTENT_START_TIME = "contentStartTime";
private static final String PROP_DISABLE_FOCUS = "disableFocus"; private static final String PROP_DISABLE_FOCUS = "disableFocus";
private static final String PROP_DISABLE_BUFFERING = "disableBuffering"; private static final String PROP_DISABLE_BUFFERING = "disableBuffering";
private static final String PROP_DISABLE_DISCONNECT_ERROR = "disableDisconnectError"; private static final String PROP_DISABLE_DISCONNECT_ERROR = "disableDisconnectError";
@ -302,6 +303,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
videoView.setBackBufferDurationMs(backBufferDurationMs); videoView.setBackBufferDurationMs(backBufferDurationMs);
} }
@ReactProp(name = PROP_CONTENT_START_TIME, defaultInt = 0)
public void setContentStartTime(final ReactExoplayerView videoView, final int contentStartTime) {
videoView.setContentStartTime(contentStartTime);
}
@ReactProp(name = PROP_DISABLE_BUFFERING, defaultBoolean = false) @ReactProp(name = PROP_DISABLE_BUFFERING, defaultBoolean = false)
public void setDisableBuffering(final ReactExoplayerView videoView, final boolean disableBuffering) { public void setDisableBuffering(final ReactExoplayerView videoView, final boolean disableBuffering) {
videoView.setDisableBuffering(disableBuffering); videoView.setDisableBuffering(disableBuffering);