mirror of
https://github.com/androidx/media.git
synced 2025-05-07 23:50:44 +08:00
Converge track selection to a single place.
This change merges the duties of FormatEvaluator into TrackSelection classes, so that both the static and dynamic parts of track selection are implemented in a single place. New feature: Demo app now allows you to enable random adaptation in the track selection dialog. Notes: - It should be quite easy to allow application side track blacklisting in addition to source side, as an extension to this. That would effectively allow applications to do seamless/deferred track selection by creating a TrackSelection with all tracks enabled, and then toggling the blacklist flags to select the ones they want to be active. - It should be trivial to implement format blacklisting for DASH and SS as an extension to this. Will do in a follow up CL. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=128707517
This commit is contained in:
parent
5eb6190682
commit
3501332dd3
@ -364,15 +364,8 @@ public class EventLogger implements ExoPlayer.EventListener, SimpleExoPlayer.Deb
|
||||
|
||||
private static String getTrackStatusString(TrackSelection selection, TrackGroup group,
|
||||
int trackIndex) {
|
||||
boolean groupEnabled = selection != null && selection.group == group;
|
||||
if (groupEnabled) {
|
||||
for (int i = 0; i < selection.length; i++) {
|
||||
if (selection.getTrack(i) == trackIndex) {
|
||||
return getTrackStatusString(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return getTrackStatusString(false);
|
||||
return getTrackStatusString(selection != null && selection.getTrackGroup() == group
|
||||
&& selection.indexOf(trackIndex) != -1);
|
||||
}
|
||||
|
||||
private static String getTrackStatusString(boolean enabled) {
|
||||
|
@ -39,8 +39,6 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.Timeline;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
@ -48,13 +46,15 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.TrackInfo;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer2.ui.PlayerControl;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
@ -138,7 +138,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
||||
private String userAgent;
|
||||
private DataSource.Factory manifestDataSourceFactory;
|
||||
private DataSource.Factory mediaDataSourceFactory;
|
||||
private FormatEvaluator.Factory formatEvaluatorFactory;
|
||||
private DefaultBandwidthMeter bandwidthMeter;
|
||||
private SimpleExoPlayer player;
|
||||
private MappingTrackSelector trackSelector;
|
||||
private TrackSelectionHelper trackSelectionHelper;
|
||||
@ -155,9 +155,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
||||
super.onCreate(savedInstanceState);
|
||||
userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
|
||||
manifestDataSourceFactory = new DefaultDataSourceFactory(this, userAgent);
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
bandwidthMeter = new DefaultBandwidthMeter();
|
||||
mediaDataSourceFactory = new DefaultDataSourceFactory(this, userAgent, bandwidthMeter);
|
||||
formatEvaluatorFactory = new AdaptiveEvaluator.Factory(bandwidthMeter);
|
||||
|
||||
mainHandler = new Handler();
|
||||
setContentView(R.layout.player_activity);
|
||||
@ -284,12 +283,15 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
eventLogger = new EventLogger();
|
||||
eventLogger.startSession();
|
||||
trackSelector = new DefaultTrackSelector(mainHandler);
|
||||
TrackSelection.Factory videoTrackSelectionFactory =
|
||||
new AdaptiveVideoTrackSelection.Factory(bandwidthMeter);
|
||||
trackSelector = new DefaultTrackSelector(mainHandler, videoTrackSelectionFactory);
|
||||
trackSelector.addListener(this);
|
||||
trackSelector.addListener(eventLogger);
|
||||
trackSelectionHelper = new TrackSelectionHelper(trackSelector);
|
||||
trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory);
|
||||
player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(),
|
||||
drmSessionManager, preferExtensionDecoders);
|
||||
player.addListener(this);
|
||||
@ -354,15 +356,14 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
||||
switch (type) {
|
||||
case Util.TYPE_SS:
|
||||
DefaultSsChunkSource.Factory factory = new DefaultSsChunkSource.Factory(
|
||||
mediaDataSourceFactory, formatEvaluatorFactory);
|
||||
mediaDataSourceFactory);
|
||||
return new SsMediaSource(uri, manifestDataSourceFactory, factory, mainHandler, eventLogger);
|
||||
case Util.TYPE_DASH:
|
||||
DefaultDashChunkSource.Factory factory2 = new DefaultDashChunkSource.Factory(
|
||||
mediaDataSourceFactory, formatEvaluatorFactory);
|
||||
mediaDataSourceFactory);
|
||||
return new DashMediaSource(uri, mediaDataSourceFactory, factory2, mainHandler, eventLogger);
|
||||
case Util.TYPE_HLS:
|
||||
return new HlsMediaSource(uri, mediaDataSourceFactory, formatEvaluatorFactory, mainHandler,
|
||||
eventLogger);
|
||||
return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger);
|
||||
case Util.TYPE_OTHER:
|
||||
return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(),
|
||||
mainHandler, eventLogger);
|
||||
|
@ -19,8 +19,10 @@ import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.RendererCapabilities;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.TrackInfo;
|
||||
import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
@ -45,6 +47,7 @@ import java.util.Locale;
|
||||
DialogInterface.OnClickListener {
|
||||
|
||||
private final MappingTrackSelector selector;
|
||||
private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory;
|
||||
|
||||
private TrackInfo trackInfo;
|
||||
private int rendererIndex;
|
||||
@ -55,13 +58,18 @@ import java.util.Locale;
|
||||
|
||||
private CheckedTextView disableView;
|
||||
private CheckedTextView defaultView;
|
||||
private CheckedTextView enableRandomAdaptationView;
|
||||
private CheckedTextView[][] trackViews;
|
||||
|
||||
/**
|
||||
* @param selector The track selector.
|
||||
* @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s,
|
||||
* or null if the selection helper should not support adaptive video.
|
||||
*/
|
||||
public TrackSelectionHelper(MappingTrackSelector selector) {
|
||||
public TrackSelectionHelper(MappingTrackSelector selector,
|
||||
TrackSelection.Factory adaptiveVideoTrackSelectionFactory) {
|
||||
this.selector = selector;
|
||||
this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,8 +88,9 @@ import java.util.Locale;
|
||||
trackGroups = trackInfo.getTrackGroups(rendererIndex);
|
||||
trackGroupsAdaptive = new boolean[trackGroups.length];
|
||||
for (int i = 0; i < trackGroups.length; i++) {
|
||||
trackGroupsAdaptive[i] = trackInfo.getAdaptiveSupport(rendererIndex, i, false)
|
||||
!= RendererCapabilities.ADAPTIVE_NOT_SUPPORTED;
|
||||
trackGroupsAdaptive[i] = adaptiveVideoTrackSelectionFactory != null
|
||||
&& (trackInfo.getAdaptiveSupport(rendererIndex, i, false)
|
||||
!= RendererCapabilities.ADAPTIVE_NOT_SUPPORTED);
|
||||
}
|
||||
isDisabled = selector.getRendererDisabled(rendererIndex);
|
||||
override = selector.hasSelectionOverride(rendererIndex, trackGroups)
|
||||
@ -118,17 +127,19 @@ import java.util.Locale;
|
||||
|
||||
// Per-track views.
|
||||
boolean haveSupportedTracks = false;
|
||||
boolean haveAdaptiveTracks = false;
|
||||
trackViews = new CheckedTextView[trackGroups.length][];
|
||||
for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
|
||||
TrackGroup group = trackGroups.get(groupIndex);
|
||||
boolean groupIsAdaptive = group.length > 1 && trackGroupsAdaptive[groupIndex];
|
||||
haveAdaptiveTracks |= groupIsAdaptive;
|
||||
trackViews[groupIndex] = new CheckedTextView[group.length];
|
||||
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
||||
if (trackIndex == 0) {
|
||||
root.addView(inflater.inflate(R.layout.list_divider, root, false));
|
||||
}
|
||||
int trackViewLayoutId = group.length < 2 || !trackGroupsAdaptive[groupIndex]
|
||||
? android.R.layout.simple_list_item_single_choice
|
||||
: android.R.layout.simple_list_item_multiple_choice;
|
||||
int trackViewLayoutId = groupIsAdaptive ? android.R.layout.simple_list_item_multiple_choice
|
||||
: android.R.layout.simple_list_item_single_choice;
|
||||
CheckedTextView trackView = (CheckedTextView) inflater.inflate(
|
||||
trackViewLayoutId, root, false);
|
||||
trackView.setText(buildTrackName(group.getFormat(trackIndex)));
|
||||
@ -148,6 +159,14 @@ import java.util.Locale;
|
||||
if (!haveSupportedTracks) {
|
||||
// Indicate that the default selection will be nothing.
|
||||
defaultView.setText(R.string.selection_default_none);
|
||||
} else if (haveAdaptiveTracks) {
|
||||
// View for using random adaptation.
|
||||
enableRandomAdaptationView = (CheckedTextView) inflater.inflate(
|
||||
android.R.layout.simple_list_item_multiple_choice, root, false);
|
||||
enableRandomAdaptationView.setText(R.string.enable_random_adaptation);
|
||||
enableRandomAdaptationView.setOnClickListener(this);
|
||||
root.addView(inflater.inflate(R.layout.list_divider, root, false));
|
||||
root.addView(enableRandomAdaptationView);
|
||||
}
|
||||
|
||||
updateViews();
|
||||
@ -158,9 +177,18 @@ import java.util.Locale;
|
||||
disableView.setChecked(isDisabled);
|
||||
defaultView.setChecked(!isDisabled && override == null);
|
||||
for (int i = 0; i < trackViews.length; i++) {
|
||||
TrackGroup trackGroup = trackGroups.get(i);
|
||||
for (int j = 0; j < trackViews[i].length; j++) {
|
||||
trackViews[i][j].setChecked(
|
||||
override != null && override.group == trackGroups.get(i) && override.indexOf(j) != -1);
|
||||
trackViews[i][j].setChecked(override != null && override.getTrackGroup() == trackGroup
|
||||
&& override.indexOf(trackGroup.getFormat(j)) != -1);
|
||||
}
|
||||
}
|
||||
if (enableRandomAdaptationView != null) {
|
||||
enableRandomAdaptationView.setEnabled(!isDisabled && override != null
|
||||
&& override.length() > 1);
|
||||
if (enableRandomAdaptationView.isEnabled()) {
|
||||
enableRandomAdaptationView.setChecked(!isDisabled
|
||||
&& override instanceof RandomTrackSelection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -191,6 +219,9 @@ import java.util.Locale;
|
||||
} else if (view == defaultView) {
|
||||
isDisabled = false;
|
||||
override = null;
|
||||
} else if (view == enableRandomAdaptationView) {
|
||||
setOverride(override.getTrackGroup(), getTracks(override),
|
||||
!enableRandomAdaptationView.isChecked());
|
||||
} else {
|
||||
isDisabled = false;
|
||||
@SuppressWarnings("unchecked")
|
||||
@ -198,31 +229,25 @@ import java.util.Locale;
|
||||
TrackGroup group = tag.first;
|
||||
int trackIndex = tag.second;
|
||||
if (!trackGroupsAdaptive[trackGroups.indexOf(group)] || override == null) {
|
||||
override = new TrackSelection(group, trackIndex);
|
||||
override = new FixedTrackSelection(group, trackIndex);
|
||||
} else {
|
||||
// The group being modified is adaptive and we already have a non-null override.
|
||||
boolean isEnabled = ((CheckedTextView) view).isChecked();
|
||||
int overrideLength = override.length();
|
||||
if (isEnabled) {
|
||||
// Remove the track from the override.
|
||||
if (override.length == 1) {
|
||||
if (overrideLength == 1) {
|
||||
// The last track is being removed, so the override becomes empty.
|
||||
override = null;
|
||||
isDisabled = true;
|
||||
} else {
|
||||
int[] tracks = new int[override.length - 1];
|
||||
int trackCount = 0;
|
||||
for (int i = 0; i < override.length; i++) {
|
||||
if (override.getTrack(i) != trackIndex) {
|
||||
tracks[trackCount++] = override.getTrack(i);
|
||||
}
|
||||
}
|
||||
override = new TrackSelection(group, tracks);
|
||||
setOverride(group, getTracksRemoving(override, trackIndex),
|
||||
enableRandomAdaptationView.isChecked());
|
||||
}
|
||||
} else {
|
||||
// Add the track to the override.
|
||||
int[] tracks = Arrays.copyOf(override.getTracks(), override.length + 1);
|
||||
tracks[tracks.length - 1] = trackIndex;
|
||||
override = new TrackSelection(group, tracks);
|
||||
setOverride(group, getTracksAdding(override, trackIndex),
|
||||
enableRandomAdaptationView.isChecked());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -230,6 +255,41 @@ import java.util.Locale;
|
||||
updateViews();
|
||||
}
|
||||
|
||||
private void setOverride(TrackGroup group, int[] tracks, boolean enableRandomAdaptation) {
|
||||
override = tracks.length == 1 ? new FixedTrackSelection(group, tracks[0])
|
||||
: (enableRandomAdaptation ? new RandomTrackSelection(group, tracks)
|
||||
: adaptiveVideoTrackSelectionFactory.createTrackSelection(group, tracks));
|
||||
}
|
||||
|
||||
// Track array manipulation.
|
||||
|
||||
private static int[] getTracks(TrackSelection trackSelection) {
|
||||
int[] tracks = new int[trackSelection.length()];
|
||||
for (int i = 0; i < tracks.length; i++) {
|
||||
tracks[i] = trackSelection.getIndexInTrackGroup(i);
|
||||
}
|
||||
return tracks;
|
||||
}
|
||||
|
||||
private static int[] getTracksAdding(TrackSelection trackSelection, int addedTrack) {
|
||||
int[] tracks = getTracks(trackSelection);
|
||||
tracks = Arrays.copyOf(tracks, tracks.length + 1);
|
||||
tracks[tracks.length - 1] = addedTrack;
|
||||
return tracks;
|
||||
}
|
||||
|
||||
private static int[] getTracksRemoving(TrackSelection trackSelection, int removedTrack) {
|
||||
int[] tracks = new int[trackSelection.length() - 1];
|
||||
int trackCount = 0;
|
||||
for (int i = 0; i < tracks.length + 1; i++) {
|
||||
int track = trackSelection.getIndexInTrackGroup(i);
|
||||
if (track != removedTrack) {
|
||||
tracks[trackCount++] = track;
|
||||
}
|
||||
}
|
||||
return tracks;
|
||||
}
|
||||
|
||||
// Track name construction.
|
||||
|
||||
private static String buildTrackName(Format format) {
|
||||
|
@ -33,6 +33,8 @@
|
||||
|
||||
<string name="selection_default_none">Default (none)</string>
|
||||
|
||||
<string name="enable_random_adaptation">Enable random adaptation</string>
|
||||
|
||||
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
||||
|
||||
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||
|
@ -834,9 +834,9 @@ import java.util.ArrayList;
|
||||
if (newSelection != null) {
|
||||
// Replace the renderer's SampleStream so the transition to playing the next period
|
||||
// can be seamless.
|
||||
Format[] formats = new Format[newSelection.length];
|
||||
Format[] formats = new Format[newSelection.length()];
|
||||
for (int j = 0; j < formats.length; j++) {
|
||||
formats[j] = newSelection.group.getFormat(newSelection.getTrack(j));
|
||||
formats[j] = newSelection.getFormat(j);
|
||||
}
|
||||
renderer.replaceStream(formats, readingPeriod.sampleStreams[i],
|
||||
readingPeriod.offsetUs);
|
||||
@ -1087,9 +1087,9 @@ import java.util.ArrayList;
|
||||
// Consider as joining only if the renderer was previously disabled.
|
||||
boolean joining = !rendererWasEnabledFlags[i] && playing;
|
||||
// Build an array of formats contained by the selection.
|
||||
Format[] formats = new Format[newSelection.length];
|
||||
Format[] formats = new Format[newSelection.length()];
|
||||
for (int j = 0; j < formats.length; j++) {
|
||||
formats[j] = newSelection.group.getFormat(newSelection.getTrack(j));
|
||||
formats[j] = newSelection.getFormat(j);
|
||||
}
|
||||
// Enable the renderer.
|
||||
renderer.enable(formats, playingPeriod.sampleStreams[i], internalPositionUs, joining,
|
||||
|
@ -250,9 +250,9 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource,
|
||||
SampleStream[] newStreams = new SampleStream[newSelections.size()];
|
||||
for (int i = 0; i < newStreams.length; i++) {
|
||||
TrackSelection selection = newSelections.get(i);
|
||||
Assertions.checkState(selection.length == 1);
|
||||
Assertions.checkState(selection.getTrack(0) == 0);
|
||||
int track = tracks.indexOf(selection.group);
|
||||
Assertions.checkState(selection.length() == 1);
|
||||
Assertions.checkState(selection.getIndexInTrackGroup(0) == 0);
|
||||
int track = tracks.indexOf(selection.getTrackGroup());
|
||||
Assertions.checkState(!trackEnabledStates[track]);
|
||||
enabledTrackCount++;
|
||||
trackEnabledStates[track] = true;
|
||||
|
@ -220,7 +220,7 @@ public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callba
|
||||
TrackGroupArray periodTrackGroups = period.getTrackGroups();
|
||||
for (int i = 0; i < allNewSelections.size(); i++) {
|
||||
TrackSelection selection = allNewSelections.get(i);
|
||||
if (periodTrackGroups.indexOf(selection.group) != -1) {
|
||||
if (periodTrackGroups.indexOf(selection.getTrackGroup()) != -1) {
|
||||
newSelectionOriginalIndices[newSelections.size()] = i;
|
||||
newSelections.add(selection);
|
||||
}
|
||||
|
@ -150,7 +150,6 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
|
||||
* This method should be called when the stream is no longer required.
|
||||
*/
|
||||
public void release() {
|
||||
chunkSource.release();
|
||||
sampleQueue.disable();
|
||||
loader.release();
|
||||
}
|
||||
|
@ -84,11 +84,4 @@ public interface ChunkSource {
|
||||
*/
|
||||
boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e);
|
||||
|
||||
/**
|
||||
* Releases the source.
|
||||
* <p>
|
||||
* This method should be called when the source is no longer required.
|
||||
*/
|
||||
void release();
|
||||
|
||||
}
|
||||
|
@ -1,380 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.chunk;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Selects from a number of available formats during playback.
|
||||
*/
|
||||
public interface FormatEvaluator {
|
||||
|
||||
/**
|
||||
* A factory for {@link FormatEvaluator} instances.
|
||||
*/
|
||||
interface Factory {
|
||||
|
||||
/**
|
||||
* Creates a {@link FormatEvaluator} instance.
|
||||
*/
|
||||
FormatEvaluator createFormatEvaluator();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the evaluator.
|
||||
*
|
||||
* @param formats The formats from which to select, ordered by decreasing bandwidth.
|
||||
*/
|
||||
void enable(Format[] formats);
|
||||
|
||||
/**
|
||||
* Disables the evaluator.
|
||||
*/
|
||||
void disable();
|
||||
|
||||
/**
|
||||
* Update the supplied evaluation.
|
||||
* <p>
|
||||
* When called, {@code evaluation} must contain the currently selected format (null for an initial
|
||||
* evaluation), the most recent reason ({@link C#SELECTION_REASON_INITIAL} for an initial
|
||||
* evaluation) and the most recent evaluation data (null for an initial evaluation).
|
||||
*
|
||||
* @param bufferedDurationUs The duration of media currently buffered in microseconds.
|
||||
* @param blacklistFlags An array whose length is equal to the number of available formats. A
|
||||
* {@code true} element indicates that a format is currently blacklisted and should not be
|
||||
* selected by the evaluation. At least one element must be {@code false}.
|
||||
* @param evaluation The evaluation to be updated.
|
||||
*/
|
||||
void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags,
|
||||
Evaluation evaluation);
|
||||
|
||||
/**
|
||||
* Evaluates whether to discard {@link MediaChunk}s from the queue.
|
||||
*
|
||||
* @param playbackPositionUs The current playback position in microseconds.
|
||||
* @param queue The queue of buffered {@link MediaChunk}s.
|
||||
* @param blacklistFlags An array whose length is equal to the number of available formats. A
|
||||
* {@code true} element indicates that a format is currently blacklisted and should not be
|
||||
* selected by the evaluation. At least one element must be {@code false}.
|
||||
* @return The preferred queue size.
|
||||
*/
|
||||
int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue,
|
||||
boolean[] blacklistFlags);
|
||||
|
||||
/**
|
||||
* A format evaluation.
|
||||
*/
|
||||
final class Evaluation {
|
||||
|
||||
/**
|
||||
* The selected format.
|
||||
*/
|
||||
public Format format;
|
||||
|
||||
/**
|
||||
* The sticky reason for the format selection.
|
||||
*/
|
||||
public int reason;
|
||||
|
||||
/**
|
||||
* Sticky optional data relating to the evaluation.
|
||||
*/
|
||||
public Object data;
|
||||
|
||||
public Evaluation() {
|
||||
reason = C.SELECTION_REASON_INITIAL;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects randomly between the available formats, excluding those that are blacklisted.
|
||||
*/
|
||||
final class RandomEvaluator implements FormatEvaluator {
|
||||
|
||||
public static class Factory implements FormatEvaluator.Factory {
|
||||
|
||||
private final int seed;
|
||||
private final boolean seedIsSet;
|
||||
|
||||
public Factory() {
|
||||
seed = 0;
|
||||
seedIsSet = false;
|
||||
}
|
||||
|
||||
public Factory(int seed) {
|
||||
this.seed = seed;
|
||||
seedIsSet = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FormatEvaluator createFormatEvaluator() {
|
||||
return seedIsSet ? new RandomEvaluator(seed) : new RandomEvaluator();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final Random random;
|
||||
|
||||
private Format[] formats;
|
||||
|
||||
public RandomEvaluator() {
|
||||
this.random = new Random();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param seed A seed for the underlying random number generator.
|
||||
*/
|
||||
public RandomEvaluator(int seed) {
|
||||
this.random = new Random(seed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable(Format[] formats) {
|
||||
this.formats = formats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
formats = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags,
|
||||
Evaluation evaluation) {
|
||||
// Count the number of non-blacklisted formats.
|
||||
int nonBlacklistedFormatCount = 0;
|
||||
for (boolean blacklistFlag : blacklistFlags) {
|
||||
if (!blacklistFlag) {
|
||||
nonBlacklistedFormatCount++;
|
||||
}
|
||||
}
|
||||
|
||||
int formatIndex = random.nextInt(nonBlacklistedFormatCount);
|
||||
if (nonBlacklistedFormatCount != formats.length) {
|
||||
// Adjust the format index to account for blacklisted formats.
|
||||
nonBlacklistedFormatCount = 0;
|
||||
for (int i = 0; i < blacklistFlags.length; i++) {
|
||||
if (!blacklistFlags[i] && formatIndex == nonBlacklistedFormatCount++) {
|
||||
formatIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Format newFormat = formats[formatIndex];
|
||||
if (evaluation.format != null && evaluation.format != newFormat) {
|
||||
evaluation.reason = C.SELECTION_REASON_ADAPTIVE;
|
||||
}
|
||||
evaluation.format = newFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue,
|
||||
boolean[] blacklistFlags) {
|
||||
return queue.size();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An adaptive evaluator for video formats, which attempts to select the best quality possible
|
||||
* given the current network conditions and state of the buffer.
|
||||
* <p>
|
||||
* This implementation should be used for video only, and should not be used for audio. It is a
|
||||
* reference implementation only. It is recommended that application developers implement their
|
||||
* own adaptive evaluator to more precisely suit their use case.
|
||||
*/
|
||||
final class AdaptiveEvaluator implements FormatEvaluator {
|
||||
|
||||
public static class Factory implements FormatEvaluator.Factory {
|
||||
|
||||
private final BandwidthMeter bandwidthMeter;
|
||||
|
||||
public Factory(BandwidthMeter bandwidthMeter) {
|
||||
this.bandwidthMeter = bandwidthMeter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FormatEvaluator createFormatEvaluator() {
|
||||
return new AdaptiveEvaluator(bandwidthMeter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final int DEFAULT_MAX_INITIAL_BITRATE = 800000;
|
||||
|
||||
private static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
|
||||
private static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
|
||||
private static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;
|
||||
private static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f;
|
||||
|
||||
private final BandwidthMeter bandwidthMeter;
|
||||
private final int maxInitialBitrate;
|
||||
private final long minDurationForQualityIncreaseUs;
|
||||
private final long maxDurationForQualityDecreaseUs;
|
||||
private final long minDurationToRetainAfterDiscardUs;
|
||||
private final float bandwidthFraction;
|
||||
|
||||
private Format[] formats;
|
||||
|
||||
/**
|
||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
||||
*/
|
||||
public AdaptiveEvaluator(BandwidthMeter bandwidthMeter) {
|
||||
this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
|
||||
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
|
||||
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
||||
* @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed
|
||||
* when bandwidthMeter cannot provide an estimate due to playback having only just started.
|
||||
* @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for
|
||||
* the evaluator to consider switching to a higher quality format.
|
||||
* @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for
|
||||
* the evaluator to consider switching to a lower quality format.
|
||||
* @param minDurationToRetainAfterDiscardMs When switching to a significantly higher quality
|
||||
* format, the evaluator may discard some of the media that it has already buffered at the
|
||||
* lower quality, so as to switch up to the higher quality faster. This is the minimum
|
||||
* duration of media that must be retained at the lower quality.
|
||||
* @param bandwidthFraction The fraction of the available bandwidth that the evaluator should
|
||||
* consider available for use. Setting to a value less than 1 is recommended to account
|
||||
* for inaccuracies in the bandwidth estimator.
|
||||
*/
|
||||
public AdaptiveEvaluator(BandwidthMeter bandwidthMeter,
|
||||
int maxInitialBitrate,
|
||||
int minDurationForQualityIncreaseMs,
|
||||
int maxDurationForQualityDecreaseMs,
|
||||
int minDurationToRetainAfterDiscardMs,
|
||||
float bandwidthFraction) {
|
||||
this.bandwidthMeter = bandwidthMeter;
|
||||
this.maxInitialBitrate = maxInitialBitrate;
|
||||
this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
|
||||
this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
|
||||
this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
|
||||
this.bandwidthFraction = bandwidthFraction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable(Format[] formats) {
|
||||
this.formats = formats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
formats = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags,
|
||||
Evaluation evaluation) {
|
||||
Format current = evaluation.format;
|
||||
Format selected = determineIdealFormat(formats, blacklistFlags,
|
||||
bandwidthMeter.getBitrateEstimate());
|
||||
if (current != null && isEnabledFormat(current, blacklistFlags)) {
|
||||
if (selected.bitrate > current.bitrate
|
||||
&& bufferedDurationUs < minDurationForQualityIncreaseUs) {
|
||||
// The ideal format is a higher quality, but we have insufficient buffer to safely switch
|
||||
// up. Defer switching up for now.
|
||||
selected = current;
|
||||
} else if (selected.bitrate < current.bitrate
|
||||
&& bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
|
||||
// The ideal format is a lower quality, but we have sufficient buffer to defer switching
|
||||
// down for now.
|
||||
selected = current;
|
||||
}
|
||||
}
|
||||
if (current != null && selected != current) {
|
||||
evaluation.reason = C.SELECTION_REASON_ADAPTIVE;
|
||||
}
|
||||
evaluation.format = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue,
|
||||
boolean[] blacklistFlags) {
|
||||
if (queue.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int queueSize = queue.size();
|
||||
long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs;
|
||||
if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) {
|
||||
return queueSize;
|
||||
}
|
||||
Format current = queue.get(queueSize - 1).trackFormat;
|
||||
Format ideal = determineIdealFormat(formats, blacklistFlags,
|
||||
bandwidthMeter.getBitrateEstimate());
|
||||
if (ideal.bitrate <= current.bitrate) {
|
||||
return queueSize;
|
||||
}
|
||||
// Discard from the first SD chunk beyond minDurationToRetainAfterDiscardUs whose resolution
|
||||
// and bitrate are both lower than the ideal format.
|
||||
for (int i = 0; i < queueSize; i++) {
|
||||
MediaChunk thisChunk = queue.get(i);
|
||||
long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
|
||||
if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
|
||||
&& thisChunk.trackFormat.bitrate < ideal.bitrate
|
||||
&& thisChunk.trackFormat.height < ideal.height
|
||||
&& thisChunk.trackFormat.height < 720
|
||||
&& thisChunk.trackFormat.width < 1280) {
|
||||
// Discard chunks from this one onwards.
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return queueSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the ideal format ignoring buffer health.
|
||||
*/
|
||||
private Format determineIdealFormat(Format[] formats, boolean[] blacklistFlags,
|
||||
long bitrateEstimate) {
|
||||
int lowestBitrateNonBlacklistedIndex = 0;
|
||||
long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE
|
||||
? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
|
||||
for (int i = 0; i < formats.length; i++) {
|
||||
Format format = formats[i];
|
||||
if (!blacklistFlags[i]) {
|
||||
if (format.bitrate <= effectiveBitrate) {
|
||||
return format;
|
||||
} else {
|
||||
lowestBitrateNonBlacklistedIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return formats[lowestBitrateNonBlacklistedIndex];
|
||||
}
|
||||
|
||||
private boolean isEnabledFormat(Format format, boolean[] blacklistFlags) {
|
||||
for (int i = 0; i < formats.length; i++) {
|
||||
if (format == formats[i]) {
|
||||
return !blacklistFlags[i];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -222,7 +222,7 @@ import java.util.List;
|
||||
|
||||
private ChunkSampleStream<DashChunkSource> buildSampleStream(TrackSelection selection,
|
||||
long positionUs) {
|
||||
int adaptationSetIndex = trackGroups.indexOf(selection.group);
|
||||
int adaptationSetIndex = trackGroups.indexOf(selection.getTrackGroup());
|
||||
AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex);
|
||||
DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource(
|
||||
manifestLoaderErrorThrower, manifest, index, adaptationSetIndex, selection,
|
||||
|
@ -28,8 +28,6 @@ import com.google.android.exoplayer2.source.chunk.Chunk;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkHolder;
|
||||
import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator.Evaluation;
|
||||
import com.google.android.exoplayer2.source.chunk.InitializationChunk;
|
||||
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk;
|
||||
@ -56,25 +54,19 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
|
||||
public static final class Factory implements DashChunkSource.Factory {
|
||||
|
||||
private final FormatEvaluator.Factory formatEvaluatorFactory;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
|
||||
public Factory(DataSource.Factory dataSourceFactory,
|
||||
FormatEvaluator.Factory formatEvaluatorFactory) {
|
||||
public Factory(DataSource.Factory dataSourceFactory) {
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.formatEvaluatorFactory = formatEvaluatorFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
DashManifest manifest, int periodIndex, int adaptationSetIndex,
|
||||
TrackSelection trackSelection, long elapsedRealtimeOffsetMs) {
|
||||
FormatEvaluator adaptiveEvaluator = trackSelection.length > 1
|
||||
? formatEvaluatorFactory.createFormatEvaluator() : null;
|
||||
DataSource dataSource = dataSourceFactory.createDataSource();
|
||||
return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex,
|
||||
adaptationSetIndex, trackSelection, dataSource, adaptiveEvaluator,
|
||||
elapsedRealtimeOffsetMs);
|
||||
return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex,
|
||||
adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs);
|
||||
}
|
||||
|
||||
}
|
||||
@ -83,16 +75,12 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
private final int adaptationSetIndex;
|
||||
private final TrackSelection trackSelection;
|
||||
private final RepresentationHolder[] representationHolders;
|
||||
private final boolean[] adaptiveFormatBlacklistFlags;
|
||||
private final DataSource dataSource;
|
||||
private final FormatEvaluator adaptiveFormatEvaluator;
|
||||
private final long elapsedRealtimeOffsetUs;
|
||||
private final Evaluation evaluation;
|
||||
|
||||
private DashManifest manifest;
|
||||
private int periodIndex;
|
||||
|
||||
private boolean lastChunkWasInitialization;
|
||||
private IOException fatalError;
|
||||
private boolean missingLastSegment;
|
||||
|
||||
@ -103,38 +91,28 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
* @param adaptationSetIndex The index of the adaptation set in the period.
|
||||
* @param trackSelection The track selection.
|
||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||
* @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats.
|
||||
* @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between
|
||||
* server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified
|
||||
* as the server's unix time minus the local elapsed time. If unknown, set to 0.
|
||||
*/
|
||||
public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection,
|
||||
DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator,
|
||||
long elapsedRealtimeOffsetMs) {
|
||||
DataSource dataSource, long elapsedRealtimeOffsetMs) {
|
||||
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
||||
this.manifest = manifest;
|
||||
this.adaptationSetIndex = adaptationSetIndex;
|
||||
this.trackSelection = trackSelection;
|
||||
this.dataSource = dataSource;
|
||||
this.adaptiveFormatEvaluator = adaptiveFormatEvaluator;
|
||||
this.periodIndex = periodIndex;
|
||||
this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetMs * 1000;
|
||||
this.evaluation = new Evaluation();
|
||||
|
||||
long periodDurationUs = getPeriodDurationUs();
|
||||
List<Representation> representations = getRepresentations();
|
||||
representationHolders = new RepresentationHolder[trackSelection.length];
|
||||
for (int i = 0; i < trackSelection.length; i++) {
|
||||
Representation representation = representations.get(trackSelection.getTrack(i));
|
||||
representationHolders = new RepresentationHolder[trackSelection.length()];
|
||||
for (int i = 0; i < representationHolders.length; i++) {
|
||||
Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
|
||||
representationHolders[i] = new RepresentationHolder(periodDurationUs, representation);
|
||||
}
|
||||
if (adaptiveFormatEvaluator != null) {
|
||||
adaptiveFormatEvaluator.enable(trackSelection.getFormats());
|
||||
adaptiveFormatBlacklistFlags = new boolean[trackSelection.length];
|
||||
} else {
|
||||
adaptiveFormatBlacklistFlags = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -144,8 +122,8 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
periodIndex = newPeriodIndex;
|
||||
long periodDurationUs = getPeriodDurationUs();
|
||||
List<Representation> representations = getRepresentations();
|
||||
for (int i = 0; i < trackSelection.length; i++) {
|
||||
Representation representation = representations.get(trackSelection.getTrack(i));
|
||||
for (int i = 0; i < representationHolders.length; i++) {
|
||||
Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
|
||||
representationHolders[i].updateRepresentation(periodDurationUs, representation);
|
||||
}
|
||||
} catch (BehindLiveWindowException e) {
|
||||
@ -164,11 +142,10 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
|
||||
@Override
|
||||
public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
|
||||
if (fatalError != null || trackSelection.length < 2) {
|
||||
if (fatalError != null || trackSelection.length() < 2) {
|
||||
return queue.size();
|
||||
}
|
||||
return adaptiveFormatEvaluator.evaluateQueueSize(playbackPositionUs, queue,
|
||||
adaptiveFormatBlacklistFlags);
|
||||
return trackSelection.evaluateQueueSize(playbackPositionUs, queue);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -177,25 +154,11 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
return;
|
||||
}
|
||||
|
||||
if (evaluation.format == null || !lastChunkWasInitialization) {
|
||||
if (trackSelection.length > 1) {
|
||||
long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0;
|
||||
adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags,
|
||||
evaluation);
|
||||
} else {
|
||||
evaluation.format = trackSelection.getFormat(0);
|
||||
evaluation.reason = C.SELECTION_REASON_UNKNOWN;
|
||||
evaluation.data = null;
|
||||
}
|
||||
}
|
||||
|
||||
Format selectedFormat = evaluation.format;
|
||||
if (selectedFormat == null) {
|
||||
return;
|
||||
}
|
||||
long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0;
|
||||
trackSelection.updateSelectedTrack(bufferedDurationUs);
|
||||
|
||||
RepresentationHolder representationHolder =
|
||||
representationHolders[trackSelection.indexOf(selectedFormat)];
|
||||
representationHolders[trackSelection.getSelectedIndex()];
|
||||
Representation selectedRepresentation = representationHolder.representation;
|
||||
DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
|
||||
|
||||
@ -211,9 +174,8 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
if (pendingInitializationUri != null || pendingIndexUri != null) {
|
||||
// We have initialization and/or index requests to make.
|
||||
Chunk initializationChunk = newInitializationChunk(representationHolder, dataSource,
|
||||
selectedFormat, evaluation.reason, evaluation.data, pendingInitializationUri,
|
||||
pendingIndexUri);
|
||||
lastChunkWasInitialization = true;
|
||||
trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(),
|
||||
trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri);
|
||||
out.chunk = initializationChunk;
|
||||
return;
|
||||
}
|
||||
@ -256,9 +218,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
return;
|
||||
}
|
||||
|
||||
Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, selectedFormat,
|
||||
evaluation.reason, evaluation.data, sampleFormat, segmentNum);
|
||||
lastChunkWasInitialization = false;
|
||||
Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource,
|
||||
trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(),
|
||||
trackSelection.getSelectionData(), sampleFormat, segmentNum);
|
||||
out.chunk = nextMediaChunk;
|
||||
}
|
||||
|
||||
@ -303,13 +265,6 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (adaptiveFormatEvaluator != null) {
|
||||
adaptiveFormatEvaluator.disable();
|
||||
}
|
||||
}
|
||||
|
||||
// Private methods.
|
||||
|
||||
private List<Representation> getRepresentations() {
|
||||
|
@ -27,11 +27,10 @@ import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.chunk.Chunk;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkHolder;
|
||||
import com.google.android.exoplayer2.source.chunk.DataChunk;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator.Evaluation;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.Variant;
|
||||
import com.google.android.exoplayer2.trackselection.BaseTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
@ -73,8 +72,6 @@ public class HlsChunkSource {
|
||||
|
||||
private final String baseUri;
|
||||
private final DataSource dataSource;
|
||||
private final FormatEvaluator formatEvaluator;
|
||||
private final Evaluation evaluation;
|
||||
private final HlsPlaylistParser playlistParser;
|
||||
private final PtsTimestampAdjusterProvider timestampAdjusterProvider;
|
||||
private final Variant[] variants;
|
||||
@ -82,8 +79,6 @@ public class HlsChunkSource {
|
||||
private final TrackGroup trackGroup;
|
||||
private final long[] variantLastPlaylistLoadTimesMs;
|
||||
|
||||
private boolean seenFirstExternalTrackSelection;
|
||||
private boolean formatEvaluatorEnabled;
|
||||
private byte[] scratchSpace;
|
||||
private boolean live;
|
||||
private long durationUs;
|
||||
@ -94,10 +89,10 @@ public class HlsChunkSource {
|
||||
private String encryptionIvString;
|
||||
private byte[] encryptionIv;
|
||||
|
||||
// Properties of enabled variants.
|
||||
// Note: The track group in the selection is typically *not* equal to trackGroup. This is due to
|
||||
// the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods
|
||||
// in TrackSelection to avoid unexpected behavior.
|
||||
private TrackSelection trackSelection;
|
||||
private long[] enabledVariantBlacklistTimes;
|
||||
private boolean[] enabledVariantBlacklistFlags;
|
||||
|
||||
/**
|
||||
* @param baseUri The playlist's base uri.
|
||||
@ -106,17 +101,14 @@ public class HlsChunkSource {
|
||||
* @param timestampAdjusterProvider A provider of {@link PtsTimestampAdjuster} instances. If
|
||||
* multiple {@link HlsChunkSource}s are used for a single playback, they should all share the
|
||||
* same provider.
|
||||
* @param formatEvaluator For adaptive tracks, selects from the available formats.
|
||||
*/
|
||||
public HlsChunkSource(String baseUri, Variant[] variants, DataSource dataSource,
|
||||
PtsTimestampAdjusterProvider timestampAdjusterProvider, FormatEvaluator formatEvaluator) {
|
||||
PtsTimestampAdjusterProvider timestampAdjusterProvider) {
|
||||
this.baseUri = baseUri;
|
||||
this.variants = variants;
|
||||
this.dataSource = dataSource;
|
||||
this.formatEvaluator = formatEvaluator;
|
||||
this.timestampAdjusterProvider = timestampAdjusterProvider;
|
||||
playlistParser = new HlsPlaylistParser();
|
||||
evaluation = new Evaluation();
|
||||
variantPlaylists = new HlsMediaPlaylist[variants.length];
|
||||
variantLastPlaylistLoadTimesMs = new long[variants.length];
|
||||
|
||||
@ -127,7 +119,7 @@ public class HlsChunkSource {
|
||||
initialTrackSelection[i] = i;
|
||||
}
|
||||
trackGroup = new TrackGroup(variantFormats);
|
||||
selectTracksInternal(new TrackSelection(trackGroup, initialTrackSelection), false);
|
||||
trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -169,7 +161,7 @@ public class HlsChunkSource {
|
||||
* @param trackSelection The track selection.
|
||||
*/
|
||||
public void selectTracks(TrackSelection trackSelection) {
|
||||
selectTracksInternal(trackSelection, true);
|
||||
this.trackSelection = trackSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -195,13 +187,14 @@ public class HlsChunkSource {
|
||||
public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, ChunkHolder out) {
|
||||
int previousChunkVariantIndex = previous != null ? trackGroup.indexOf(previous.trackFormat)
|
||||
: -1;
|
||||
updateFormatEvaluation(previous, playbackPositionUs);
|
||||
int newVariantIndex = trackGroup.indexOf(evaluation.format);
|
||||
updateSelectedTrack(previous, playbackPositionUs);
|
||||
int newVariantIndex = trackSelection.getSelectedIndexInTrackGroup();
|
||||
boolean switchingVariant = previousChunkVariantIndex != newVariantIndex;
|
||||
HlsMediaPlaylist mediaPlaylist = variantPlaylists[newVariantIndex];
|
||||
if (mediaPlaylist == null) {
|
||||
// We don't have the media playlist for the next variant. Request it now.
|
||||
out.chunk = newMediaPlaylistChunk(newVariantIndex, evaluation.reason, evaluation.data);
|
||||
out.chunk = newMediaPlaylistChunk(newVariantIndex, trackSelection.getSelectionReason(),
|
||||
trackSelection.getSelectionData());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -236,7 +229,8 @@ public class HlsChunkSource {
|
||||
if (!mediaPlaylist.live) {
|
||||
out.endOfStream = true;
|
||||
} else if (shouldRerequestLiveMediaPlaylist(newVariantIndex)) {
|
||||
out.chunk = newMediaPlaylistChunk(newVariantIndex, evaluation.reason, evaluation.data);
|
||||
out.chunk = newMediaPlaylistChunk(newVariantIndex,
|
||||
trackSelection.getSelectionReason(), trackSelection.getSelectionData());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -250,7 +244,7 @@ public class HlsChunkSource {
|
||||
if (!keyUri.equals(encryptionKeyUri)) {
|
||||
// Encryption is specified and the key has changed.
|
||||
out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, newVariantIndex,
|
||||
evaluation.reason, evaluation.data);
|
||||
trackSelection.getSelectionReason(), trackSelection.getSelectionData());
|
||||
return;
|
||||
}
|
||||
if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) {
|
||||
@ -332,7 +326,8 @@ public class HlsChunkSource {
|
||||
extractorNeedsInit = false;
|
||||
}
|
||||
|
||||
out.chunk = new HlsMediaChunk(dataSource, dataSpec, format, evaluation.reason, evaluation.data,
|
||||
out.chunk = new HlsMediaChunk(dataSource, dataSpec, format,
|
||||
trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
|
||||
startTimeUs, endTimeUs, chunkMediaSequence, segment.discontinuitySequenceNumber, extractor,
|
||||
extractorNeedsInit, switchingVariant, encryptionKey, encryptionIv);
|
||||
}
|
||||
@ -412,86 +407,29 @@ public class HlsChunkSource {
|
||||
InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e;
|
||||
int responseCode = responseCodeException.responseCode;
|
||||
if (responseCode == 404 || responseCode == 410) {
|
||||
int enabledVariantIndex = trackSelection.indexOf(chunk.trackFormat);
|
||||
boolean alreadyBlacklisted = enabledVariantBlacklistFlags[enabledVariantIndex];
|
||||
enabledVariantBlacklistFlags[enabledVariantIndex] = true;
|
||||
enabledVariantBlacklistTimes[enabledVariantIndex] = SystemClock.elapsedRealtime();
|
||||
if (alreadyBlacklisted) {
|
||||
// The playlist was already blacklisted.
|
||||
Log.w(TAG, "Already blacklisted variant (" + responseCode + "): "
|
||||
+ chunk.dataSpec.uri);
|
||||
return false;
|
||||
} else if (!allEnabledVariantsBlacklisted()) {
|
||||
// We've handled the 404/410 by blacklisting the variant.
|
||||
Log.w(TAG, "Blacklisted variant (" + responseCode + "): "
|
||||
+ chunk.dataSpec.uri);
|
||||
return true;
|
||||
} else {
|
||||
// This was the last non-blacklisted playlist. Don't blacklist it.
|
||||
Log.w(TAG, "Final variant not blacklisted (" + responseCode + "): "
|
||||
+ chunk.dataSpec.uri);
|
||||
enabledVariantBlacklistFlags[enabledVariantIndex] = false;
|
||||
return false;
|
||||
int trackSelectionIndex = trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat));
|
||||
if (trackSelectionIndex != -1) {
|
||||
boolean blacklisted = trackSelection.blacklist(trackSelectionIndex,
|
||||
DEFAULT_PLAYLIST_BLACKLIST_MS);
|
||||
if (blacklisted) {
|
||||
// We've handled the 404/410 by blacklisting the variant.
|
||||
Log.w(TAG, "Blacklisted variant (" + responseCode + "): " + chunk.dataSpec.uri);
|
||||
return true;
|
||||
} else {
|
||||
// This was the last non-blacklisted playlist. Don't blacklist it.
|
||||
Log.w(TAG, "Final variant not blacklisted (" + responseCode + "): "
|
||||
+ chunk.dataSpec.uri);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
disableFormatEvaluator();
|
||||
}
|
||||
|
||||
// Private methods.
|
||||
|
||||
private void selectTracksInternal(TrackSelection trackSelection, boolean isExternal) {
|
||||
this.trackSelection = trackSelection;
|
||||
seenFirstExternalTrackSelection |= isExternal;
|
||||
|
||||
// Reset the enabled variant blacklist flags.
|
||||
enabledVariantBlacklistTimes = new long[trackSelection.length];
|
||||
enabledVariantBlacklistFlags = new boolean[trackSelection.length];
|
||||
|
||||
if (!isExternal) {
|
||||
return;
|
||||
}
|
||||
|
||||
disableFormatEvaluator();
|
||||
if (trackSelection.length > 1) {
|
||||
Format[] formats = trackSelection.getFormats();
|
||||
formatEvaluator.enable(formats);
|
||||
formatEvaluatorEnabled = true;
|
||||
if (!Util.contains(formats, evaluation.format)) {
|
||||
evaluation.format = null;
|
||||
}
|
||||
} else {
|
||||
evaluation.reason = C.SELECTION_REASON_UNKNOWN;
|
||||
evaluation.data = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFormatEvaluation(HlsMediaChunk previous, long playbackPositionUs) {
|
||||
clearStaleBlacklistedVariants();
|
||||
if (!seenFirstExternalTrackSelection) {
|
||||
if (!enabledVariantBlacklistFlags[trackSelection.indexOf(variants[0].format)]) {
|
||||
// Use the first variant prior to external track selection, unless it's been blacklisted.
|
||||
evaluation.format = variants[0].format;
|
||||
return;
|
||||
}
|
||||
// Try from lowest bitrate to highest.
|
||||
for (int i = trackSelection.length - 1; i >= 0; i--) {
|
||||
if (!enabledVariantBlacklistFlags[i]) {
|
||||
evaluation.format = trackSelection.getFormat(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Should never happen.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (trackSelection.length == 1) {
|
||||
evaluation.format = trackSelection.getFormat(0);
|
||||
return;
|
||||
}
|
||||
private void updateSelectedTrack(HlsMediaChunk previous, long playbackPositionUs) {
|
||||
long bufferedDurationUs;
|
||||
if (previous != null) {
|
||||
// Use start time of the previous chunk rather than its end time because switching format
|
||||
@ -500,7 +438,7 @@ public class HlsChunkSource {
|
||||
} else {
|
||||
bufferedDurationUs = 0;
|
||||
}
|
||||
formatEvaluator.evaluateFormat(bufferedDurationUs, enabledVariantBlacklistFlags, evaluation);
|
||||
trackSelection.updateSelectedTrack(bufferedDurationUs);
|
||||
}
|
||||
|
||||
private boolean shouldRerequestLiveMediaPlaylist(int variantIndex) {
|
||||
@ -562,34 +500,54 @@ public class HlsChunkSource {
|
||||
durationUs = live ? C.UNSET_TIME_US : mediaPlaylist.durationUs;
|
||||
}
|
||||
|
||||
private boolean allEnabledVariantsBlacklisted() {
|
||||
for (boolean enabledVariantBlacklistFlag : enabledVariantBlacklistFlags) {
|
||||
if (!enabledVariantBlacklistFlag) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void clearStaleBlacklistedVariants() {
|
||||
long currentTime = SystemClock.elapsedRealtime();
|
||||
for (int i = 0; i < enabledVariantBlacklistFlags.length; i++) {
|
||||
if (enabledVariantBlacklistFlags[i]
|
||||
&& currentTime - enabledVariantBlacklistTimes[i] > DEFAULT_PLAYLIST_BLACKLIST_MS) {
|
||||
enabledVariantBlacklistFlags[i] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void disableFormatEvaluator() {
|
||||
if (formatEvaluatorEnabled) {
|
||||
formatEvaluator.disable();
|
||||
formatEvaluatorEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Private classes.
|
||||
|
||||
/**
|
||||
* A {@link TrackSelection} to use for initialization.
|
||||
*/
|
||||
public static final class InitializationTrackSelection extends BaseTrackSelection {
|
||||
|
||||
private int selectedIndex;
|
||||
|
||||
public InitializationTrackSelection(TrackGroup group, int[] tracks) {
|
||||
super(group, tracks);
|
||||
selectedIndex = indexOf(group.getFormat(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSelectedTrack(long bufferedDurationUs) {
|
||||
long nowMs = SystemClock.elapsedRealtime();
|
||||
if (!isBlacklisted(selectedIndex, nowMs)) {
|
||||
return;
|
||||
}
|
||||
// Try from lowest bitrate to highest.
|
||||
for (int i = length - 1; i >= 0; i--) {
|
||||
if (!isBlacklisted(i, nowMs)) {
|
||||
selectedIndex = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Should never happen.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSelectedIndex() {
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSelectionReason() {
|
||||
return C.SELECTION_REASON_UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSelectionData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class MediaPlaylistChunk extends DataChunk {
|
||||
|
||||
public final int variantIndex;
|
||||
|
@ -28,7 +28,6 @@ import com.google.android.exoplayer2.source.SinglePeriodTimeline;
|
||||
import com.google.android.exoplayer2.source.Timeline;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
|
||||
@ -64,7 +63,6 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
|
||||
|
||||
private final Uri manifestUri;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final FormatEvaluator.Factory formatEvaluatorFactory;
|
||||
private final int minLoadableRetryCount;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final IdentityHashMap<SampleStream, HlsSampleStreamWrapper> sampleStreamSources;
|
||||
@ -89,19 +87,17 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
|
||||
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
|
||||
private CompositeSequenceableLoader sequenceableLoader;
|
||||
|
||||
public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory,
|
||||
FormatEvaluator.Factory formatEvaluatorFactory, Handler eventHandler,
|
||||
public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler,
|
||||
AdaptiveMediaSourceEventListener eventListener) {
|
||||
this(manifestUri, dataSourceFactory, formatEvaluatorFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT,
|
||||
eventHandler, eventListener);
|
||||
this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler,
|
||||
eventListener);
|
||||
}
|
||||
|
||||
public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory,
|
||||
FormatEvaluator.Factory formatEvaluatorFactory, int minLoadableRetryCount,
|
||||
Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) {
|
||||
int minLoadableRetryCount, Handler eventHandler,
|
||||
AdaptiveMediaSourceEventListener eventListener) {
|
||||
this.manifestUri = manifestUri;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.formatEvaluatorFactory = formatEvaluatorFactory;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
|
||||
@ -352,7 +348,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
|
||||
Format.NO_VALUE);
|
||||
Variant[] variants = new Variant[] {new Variant(playlist.baseUri, format, null)};
|
||||
sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants,
|
||||
formatEvaluatorFactory.createFormatEvaluator(), null, null));
|
||||
null, null));
|
||||
return sampleStreamWrappers;
|
||||
}
|
||||
|
||||
@ -386,8 +382,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
|
||||
Variant[] variants = new Variant[selectedVariants.size()];
|
||||
selectedVariants.toArray(variants);
|
||||
sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants,
|
||||
formatEvaluatorFactory.createFormatEvaluator(), masterPlaylist.muxedAudioFormat,
|
||||
masterPlaylist.muxedCaptionFormat));
|
||||
masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat));
|
||||
}
|
||||
|
||||
// Build the audio stream wrapper if applicable.
|
||||
@ -396,7 +391,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
|
||||
Variant[] variants = new Variant[audioVariants.size()];
|
||||
audioVariants.toArray(variants);
|
||||
sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, baseUri, variants, null,
|
||||
null, null));
|
||||
null));
|
||||
}
|
||||
|
||||
// Build the text stream wrapper if applicable.
|
||||
@ -405,18 +400,17 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
|
||||
Variant[] variants = new Variant[subtitleVariants.size()];
|
||||
subtitleVariants.toArray(variants);
|
||||
sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, baseUri, variants, null,
|
||||
null, null));
|
||||
null));
|
||||
}
|
||||
|
||||
return sampleStreamWrappers;
|
||||
}
|
||||
|
||||
private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, String baseUri,
|
||||
Variant[] variants, FormatEvaluator formatEvaluator, Format muxedAudioFormat,
|
||||
Format muxedCaptionFormat) {
|
||||
Variant[] variants, Format muxedAudioFormat, Format muxedCaptionFormat) {
|
||||
DataSource dataSource = dataSourceFactory.createDataSource();
|
||||
HlsChunkSource defaultChunkSource = new HlsChunkSource(baseUri, variants, dataSource,
|
||||
timestampAdjusterProvider, formatEvaluator);
|
||||
timestampAdjusterProvider);
|
||||
return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator,
|
||||
preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount,
|
||||
eventDispatcher);
|
||||
@ -440,7 +434,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
|
||||
int[] newSelectionOriginalIndices = new int[allNewSelections.size()];
|
||||
for (int i = 0; i < allNewSelections.size(); i++) {
|
||||
TrackSelection selection = allNewSelections.get(i);
|
||||
if (sampleStreamWrapperTrackGroups.indexOf(selection.group) != -1) {
|
||||
if (sampleStreamWrapperTrackGroups.indexOf(selection.getTrackGroup()) != -1) {
|
||||
newSelectionOriginalIndices[newSelections.size()] = i;
|
||||
newSelections.add(selection);
|
||||
}
|
||||
|
@ -168,11 +168,10 @@ import java.util.List;
|
||||
SampleStream[] newStreams = new SampleStream[newSelections.size()];
|
||||
for (int i = 0; i < newStreams.length; i++) {
|
||||
TrackSelection selection = newSelections.get(i);
|
||||
int group = trackGroups.indexOf(selection.group);
|
||||
int[] tracks = selection.getTracks();
|
||||
int group = trackGroups.indexOf(selection.getTrackGroup());
|
||||
setTrackGroupEnabledState(group, true);
|
||||
if (group == primaryTrackGroupIndex) {
|
||||
chunkSource.selectTracks(new TrackSelection(chunkSource.getTrackGroup(), tracks));
|
||||
chunkSource.selectTracks(selection);
|
||||
}
|
||||
newStreams[i] = new SampleStreamImpl(group);
|
||||
}
|
||||
@ -236,7 +235,6 @@ import java.util.List;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
chunkSource.release();
|
||||
int sampleQueueCount = sampleQueues.size();
|
||||
for (int i = 0; i < sampleQueueCount; i++) {
|
||||
sampleQueues.valueAt(i).disable();
|
||||
|
@ -25,8 +25,6 @@ import com.google.android.exoplayer2.source.chunk.Chunk;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkHolder;
|
||||
import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator.Evaluation;
|
||||
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;
|
||||
@ -47,24 +45,19 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
|
||||
public static final class Factory implements SsChunkSource.Factory {
|
||||
|
||||
private final FormatEvaluator.Factory formatEvaluatorFactory;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
|
||||
public Factory(DataSource.Factory dataSourceFactory,
|
||||
FormatEvaluator.Factory formatEvaluatorFactory) {
|
||||
public Factory(DataSource.Factory dataSourceFactory) {
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.formatEvaluatorFactory = formatEvaluatorFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SsChunkSource createChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
SsManifest manifest, int elementIndex, TrackSelection trackSelection,
|
||||
TrackEncryptionBox[] trackEncryptionBoxes) {
|
||||
FormatEvaluator adaptiveEvaluator = trackSelection.length > 1
|
||||
? formatEvaluatorFactory.createFormatEvaluator() : null;
|
||||
DataSource dataSource = dataSourceFactory.createDataSource();
|
||||
return new DefaultSsChunkSource(manifestLoaderErrorThrower, manifest, elementIndex,
|
||||
trackSelection, dataSource, adaptiveEvaluator, trackEncryptionBoxes);
|
||||
trackSelection, dataSource, trackEncryptionBoxes);
|
||||
}
|
||||
|
||||
}
|
||||
@ -73,10 +66,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
private final int elementIndex;
|
||||
private final TrackSelection trackSelection;
|
||||
private final ChunkExtractorWrapper[] extractorWrappers;
|
||||
private final boolean[] adaptiveFormatBlacklistFlags;
|
||||
private final DataSource dataSource;
|
||||
private final Evaluation evaluation;
|
||||
private final FormatEvaluator adaptiveFormatEvaluator;
|
||||
|
||||
private SsManifest manifest;
|
||||
private int currentManifestChunkOffset;
|
||||
@ -89,26 +79,23 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
* @param elementIndex The index of the stream element in the manifest.
|
||||
* @param trackSelection The track selection.
|
||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||
* @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats.
|
||||
* @param trackEncryptionBoxes Track encryption boxes for the stream.
|
||||
*/
|
||||
public DefaultSsChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, SsManifest manifest,
|
||||
int elementIndex, TrackSelection trackSelection, DataSource dataSource,
|
||||
FormatEvaluator adaptiveFormatEvaluator, TrackEncryptionBox[] trackEncryptionBoxes) {
|
||||
TrackEncryptionBox[] trackEncryptionBoxes) {
|
||||
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
||||
this.manifest = manifest;
|
||||
this.elementIndex = elementIndex;
|
||||
this.trackSelection = trackSelection;
|
||||
this.dataSource = dataSource;
|
||||
this.adaptiveFormatEvaluator = adaptiveFormatEvaluator;
|
||||
this.evaluation = new Evaluation();
|
||||
|
||||
StreamElement streamElement = manifest.streamElements[elementIndex];
|
||||
|
||||
extractorWrappers = new ChunkExtractorWrapper[trackSelection.length];
|
||||
for (int i = 0; i < trackSelection.length; i++) {
|
||||
int manifestTrackIndex = trackSelection.getTrack(i);
|
||||
Format format = trackSelection.getFormat(i);
|
||||
extractorWrappers = new ChunkExtractorWrapper[trackSelection.length()];
|
||||
for (int i = 0; i < extractorWrappers.length; i++) {
|
||||
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i);
|
||||
Format format = streamElement.formats[i];
|
||||
int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : -1;
|
||||
Track track = new Track(manifestTrackIndex, streamElement.type, streamElement.timescale,
|
||||
C.UNSET_TIME_US, manifest.durationUs, format, Track.TRANSFORMATION_NONE,
|
||||
@ -118,12 +105,6 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
| FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track);
|
||||
extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, false);
|
||||
}
|
||||
if (adaptiveFormatEvaluator != null) {
|
||||
adaptiveFormatEvaluator.enable(trackSelection.getFormats());
|
||||
adaptiveFormatBlacklistFlags = new boolean[trackSelection.length];
|
||||
} else {
|
||||
adaptiveFormatBlacklistFlags = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -162,11 +143,10 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
|
||||
@Override
|
||||
public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
|
||||
if (fatalError != null || trackSelection.length < 2) {
|
||||
if (fatalError != null || trackSelection.length() < 2) {
|
||||
return queue.size();
|
||||
}
|
||||
return adaptiveFormatEvaluator.evaluateQueueSize(playbackPositionUs, queue,
|
||||
adaptiveFormatBlacklistFlags);
|
||||
return trackSelection.evaluateQueueSize(playbackPositionUs, queue);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -175,20 +155,8 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
return;
|
||||
}
|
||||
|
||||
if (trackSelection.length > 1) {
|
||||
long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0;
|
||||
adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags,
|
||||
evaluation);
|
||||
} else {
|
||||
evaluation.format = trackSelection.getFormat(0);
|
||||
evaluation.reason = C.SELECTION_REASON_UNKNOWN;
|
||||
evaluation.data = null;
|
||||
}
|
||||
|
||||
Format selectedFormat = evaluation.format;
|
||||
if (selectedFormat == null) {
|
||||
return;
|
||||
}
|
||||
long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0;
|
||||
trackSelection.updateSelectedTrack(bufferedDurationUs);
|
||||
|
||||
StreamElement streamElement = manifest.streamElements[elementIndex];
|
||||
if (streamElement.chunkCount == 0) {
|
||||
@ -219,14 +187,15 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex);
|
||||
int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset;
|
||||
|
||||
int trackSelectionIndex = trackSelection.indexOf(selectedFormat);
|
||||
int trackSelectionIndex = trackSelection.getSelectedIndex();
|
||||
ChunkExtractorWrapper extractorWrapper = extractorWrappers[trackSelectionIndex];
|
||||
|
||||
int manifestTrackIndex = trackSelection.getTrack(trackSelectionIndex);
|
||||
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex);
|
||||
Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex);
|
||||
|
||||
out.chunk = newMediaChunk(selectedFormat, dataSource, uri, null, currentAbsoluteChunkIndex,
|
||||
chunkStartTimeUs, chunkEndTimeUs, evaluation.reason, evaluation.data, extractorWrapper);
|
||||
out.chunk = newMediaChunk(trackSelection.getSelectedFormat(), dataSource, uri, null,
|
||||
currentAbsoluteChunkIndex, chunkStartTimeUs, chunkEndTimeUs,
|
||||
trackSelection.getSelectionReason(), trackSelection.getSelectionData(), extractorWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -240,13 +209,6 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (adaptiveFormatEvaluator != null) {
|
||||
adaptiveFormatEvaluator.disable();
|
||||
}
|
||||
}
|
||||
|
||||
// Private methods.
|
||||
|
||||
private static MediaChunk newMediaChunk(Format format, DataSource dataSource, Uri uri,
|
||||
|
@ -199,7 +199,7 @@ import java.util.List;
|
||||
|
||||
private ChunkSampleStream<SsChunkSource> buildSampleStream(TrackSelection selection,
|
||||
long positionUs) {
|
||||
int streamElementIndex = trackGroups.indexOf(selection.group);
|
||||
int streamElementIndex = trackGroups.indexOf(selection.getTrackGroup());
|
||||
SsChunkSource chunkSource = chunkSourceFactory.createChunkSource(manifestLoaderErrorThrower,
|
||||
manifest, streamElementIndex, selection, trackEncryptionBoxes);
|
||||
return new ChunkSampleStream<>(manifest.streamElements[streamElementIndex].type, chunkSource,
|
||||
|
@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.trackselection;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A bandwidth based adaptive {@link TrackSelection} for video, whose selected track is updated to
|
||||
* be the one of highest quality given the current network conditions and the state of the buffer.
|
||||
*/
|
||||
public class AdaptiveVideoTrackSelection extends BaseTrackSelection {
|
||||
|
||||
/**
|
||||
* Factory for {@link AdaptiveVideoTrackSelection} instances.
|
||||
*/
|
||||
public static final class Factory implements TrackSelection.Factory {
|
||||
|
||||
private final BandwidthMeter bandwidthMeter;
|
||||
private final int maxInitialBitrate;
|
||||
private final int minDurationForQualityIncreaseMs;
|
||||
private final int maxDurationForQualityDecreaseMs;
|
||||
private final int minDurationToRetainAfterDiscardMs;
|
||||
private final float bandwidthFraction;
|
||||
|
||||
/**
|
||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
||||
*/
|
||||
public Factory(BandwidthMeter bandwidthMeter) {
|
||||
this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
|
||||
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
|
||||
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
||||
* @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed
|
||||
* when a bandwidth estimate is unavailable.
|
||||
* @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for
|
||||
* the selected track to switch to one of higher quality.
|
||||
* @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for
|
||||
* the selected track to switch to one of lower quality.
|
||||
* @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
|
||||
* quality, the selection may indicate that media already buffered at the lower quality can
|
||||
* be discarded to speed up the switch. This is the minimum duration of media that must be
|
||||
* retained at the lower quality.
|
||||
* @param bandwidthFraction The fraction of the available bandwidth that the selection should
|
||||
* consider available for use. Setting to a value less than 1 is recommended to account
|
||||
* for inaccuracies in the bandwidth estimator.
|
||||
*/
|
||||
public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate,
|
||||
int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs,
|
||||
int minDurationToRetainAfterDiscardMs, float bandwidthFraction) {
|
||||
this.bandwidthMeter = bandwidthMeter;
|
||||
this.maxInitialBitrate = maxInitialBitrate;
|
||||
this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs;
|
||||
this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs;
|
||||
this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs;
|
||||
this.bandwidthFraction = bandwidthFraction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdaptiveVideoTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
|
||||
return new AdaptiveVideoTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate,
|
||||
minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs,
|
||||
minDurationToRetainAfterDiscardMs, bandwidthFraction);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000;
|
||||
public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
|
||||
public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
|
||||
public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;
|
||||
public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f;
|
||||
|
||||
private final BandwidthMeter bandwidthMeter;
|
||||
private final int maxInitialBitrate;
|
||||
private final long minDurationForQualityIncreaseUs;
|
||||
private final long maxDurationForQualityDecreaseUs;
|
||||
private final long minDurationToRetainAfterDiscardUs;
|
||||
private final float bandwidthFraction;
|
||||
|
||||
private int selectedIndex;
|
||||
private int reason;
|
||||
|
||||
/**
|
||||
* @param group The {@link TrackGroup}. Must not be null.
|
||||
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
|
||||
* null or empty. May be in any order.
|
||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
||||
*/
|
||||
public AdaptiveVideoTrackSelection(TrackGroup group, int[] tracks,
|
||||
BandwidthMeter bandwidthMeter) {
|
||||
this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
|
||||
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
|
||||
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param group The {@link TrackGroup}. Must not be null.
|
||||
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
|
||||
* null or empty. May be in any order.
|
||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
||||
* @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a
|
||||
* bandwidth estimate is unavailable.
|
||||
* @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
|
||||
* selected track to switch to one of higher quality.
|
||||
* @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
|
||||
* selected track to switch to one of lower quality.
|
||||
* @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
|
||||
* quality, the selection may indicate that media already buffered at the lower quality can
|
||||
* be discarded to speed up the switch. This is the minimum duration of media that must be
|
||||
* retained at the lower quality.
|
||||
* @param bandwidthFraction The fraction of the available bandwidth that the selection should
|
||||
* consider available for use. Setting to a value less than 1 is recommended to account
|
||||
* for inaccuracies in the bandwidth estimator.
|
||||
*/
|
||||
public AdaptiveVideoTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter,
|
||||
int maxInitialBitrate, int minDurationForQualityIncreaseMs,
|
||||
int maxDurationForQualityDecreaseMs, int minDurationToRetainAfterDiscardMs,
|
||||
float bandwidthFraction) {
|
||||
super(group, tracks);
|
||||
this.bandwidthMeter = bandwidthMeter;
|
||||
this.maxInitialBitrate = maxInitialBitrate;
|
||||
this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
|
||||
this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
|
||||
this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
|
||||
this.bandwidthFraction = bandwidthFraction;
|
||||
selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE);
|
||||
reason = C.SELECTION_REASON_INITIAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSelectedTrack(long bufferedDurationUs) {
|
||||
long nowMs = SystemClock.elapsedRealtime();
|
||||
// Get the current and ideal selections.
|
||||
int currentSelectedIndex = selectedIndex;
|
||||
Format currentFormat = getSelectedFormat();
|
||||
int idealSelectedIndex = determineIdealSelectedIndex(nowMs);
|
||||
Format idealFormat = getFormat(idealSelectedIndex);
|
||||
// Assume we can switch to the ideal selection.
|
||||
selectedIndex = idealSelectedIndex;
|
||||
// Revert back to the current selection if conditions are not suitable for switching.
|
||||
if (currentFormat != null && !isBlacklisted(selectedIndex, nowMs)) {
|
||||
if (idealFormat.bitrate > currentFormat.bitrate
|
||||
&& bufferedDurationUs < minDurationForQualityIncreaseUs) {
|
||||
// The ideal track is a higher quality, but we have insufficient buffer to safely switch
|
||||
// up. Defer switching up for now.
|
||||
selectedIndex = currentSelectedIndex;
|
||||
} else if (idealFormat.bitrate < currentFormat.bitrate
|
||||
&& bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
|
||||
// The ideal track is a lower quality, but we have sufficient buffer to defer switching
|
||||
// down for now.
|
||||
selectedIndex = currentSelectedIndex;
|
||||
}
|
||||
}
|
||||
// If we adapted, update the trigger.
|
||||
if (selectedIndex != currentSelectedIndex) {
|
||||
reason = C.SELECTION_REASON_ADAPTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSelectedIndex() {
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSelectionReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSelectionData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
|
||||
if (queue.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int queueSize = queue.size();
|
||||
long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs;
|
||||
if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) {
|
||||
return queueSize;
|
||||
}
|
||||
int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime());
|
||||
Format idealFormat = getFormat(idealSelectedIndex);
|
||||
// Discard from the first SD chunk beyond minDurationToRetainAfterDiscardUs whose resolution and
|
||||
// bitrate are both lower than the ideal track.
|
||||
for (int i = 0; i < queueSize; i++) {
|
||||
MediaChunk chunk = queue.get(i);
|
||||
long durationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs;
|
||||
if (durationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs
|
||||
&& chunk.trackFormat.bitrate < idealFormat.bitrate
|
||||
&& chunk.trackFormat.height < idealFormat.height
|
||||
&& chunk.trackFormat.height < 720 && chunk.trackFormat.width < 1280) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return queueSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the ideal selected index ignoring buffer health.
|
||||
*
|
||||
* @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}, or
|
||||
* {@link Long#MIN_VALUE} to ignore blacklisting.
|
||||
*/
|
||||
private int determineIdealSelectedIndex(long nowMs) {
|
||||
long bitrateEstimate = bandwidthMeter.getBitrateEstimate();
|
||||
long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE
|
||||
? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
|
||||
int lowestBitrateNonBlacklistedIndex = 0;
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {
|
||||
Format format = getFormat(i);
|
||||
if (format.bitrate <= effectiveBitrate) {
|
||||
return i;
|
||||
} else {
|
||||
lowestBitrateNonBlacklistedIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return lowestBitrateNonBlacklistedIndex;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.trackselection;
|
||||
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.Format.DecreasingBandwidthComparator;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An abstract base class suitable for most {@link TrackSelection} implementations.
|
||||
*/
|
||||
public abstract class BaseTrackSelection implements TrackSelection {
|
||||
|
||||
/**
|
||||
* The selected {@link TrackGroup}.
|
||||
*/
|
||||
protected final TrackGroup group;
|
||||
/**
|
||||
* The number of selected tracks within the {@link TrackGroup}. Always greater than zero.
|
||||
*/
|
||||
protected final int length;
|
||||
|
||||
/**
|
||||
* The indices of the selected tracks in {@link #group}, in order of decreasing bandwidth.
|
||||
*/
|
||||
private final int[] tracks;
|
||||
/**
|
||||
* The {@link Format}s of the selected tracks, in order of decreasing bandwidth.
|
||||
*/
|
||||
private final Format[] formats;
|
||||
/**
|
||||
* Selected track blacklist timestamps, in order of decreasing bandwidth.
|
||||
*/
|
||||
private final long[] blacklistUntilTimes;
|
||||
|
||||
// Lazily initialized hashcode.
|
||||
private int hashCode;
|
||||
|
||||
/**
|
||||
* @param group The {@link TrackGroup}. Must not be null.
|
||||
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
|
||||
* null or empty. May be in any order.
|
||||
*/
|
||||
public BaseTrackSelection(TrackGroup group, int... tracks) {
|
||||
Assertions.checkState(tracks.length > 0);
|
||||
this.group = Assertions.checkNotNull(group);
|
||||
this.length = tracks.length;
|
||||
// Set the formats, sorted in order of decreasing bandwidth.
|
||||
formats = new Format[length];
|
||||
for (int i = 0; i < tracks.length; i++) {
|
||||
formats[i] = group.getFormat(tracks[i]);
|
||||
}
|
||||
Arrays.sort(formats, new DecreasingBandwidthComparator());
|
||||
// Set the format indices in the same order.
|
||||
this.tracks = new int[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
this.tracks[i] = group.indexOf(formats[i]);
|
||||
}
|
||||
blacklistUntilTimes = new long[length];
|
||||
}
|
||||
|
||||
@Override
|
||||
public final TrackGroup getTrackGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int length() {
|
||||
return tracks.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Format getFormat(int index) {
|
||||
return formats[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getIndexInTrackGroup(int index) {
|
||||
return tracks[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int indexOf(Format format) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (formats[i] == format) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int indexOf(int indexInTrackGroup) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (tracks[i] == indexInTrackGroup) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Format getSelectedFormat() {
|
||||
return formats[getSelectedIndex()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getSelectedIndexInTrackGroup() {
|
||||
return tracks[getSelectedIndex()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
|
||||
return queue.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean blacklist(int index, long blacklistDurationMs) {
|
||||
long nowMs = SystemClock.elapsedRealtime();
|
||||
boolean canBlacklist = isBlacklisted(index, nowMs);
|
||||
for (int i = 0; i < length && !canBlacklist; i++) {
|
||||
canBlacklist = i != index && !isBlacklisted(index, nowMs);
|
||||
}
|
||||
if (!canBlacklist) {
|
||||
return false;
|
||||
}
|
||||
blacklistUntilTimes[index] = Math.max(blacklistUntilTimes[index], nowMs + blacklistDurationMs);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the track at the specified index in the selection is blaclisted.
|
||||
*
|
||||
* @param index The index of the track in the selection.
|
||||
* @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}.
|
||||
*/
|
||||
protected final boolean isBlacklisted(int index, long nowMs) {
|
||||
return blacklistUntilTimes[index] > nowMs;
|
||||
}
|
||||
|
||||
// Object overrides.
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (hashCode == 0) {
|
||||
hashCode = 31 * System.identityHashCode(group) + Arrays.hashCode(tracks);
|
||||
}
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
BaseTrackSelection other = (BaseTrackSelection) obj;
|
||||
return group == other.group && Arrays.equals(tracks, other.tracks);
|
||||
}
|
||||
|
||||
}
|
@ -51,6 +51,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
private static final int[] NO_TRACKS = new int[0];
|
||||
private static final String TAG = "DefaultTrackSelector";
|
||||
|
||||
private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory;
|
||||
|
||||
// Audio and text.
|
||||
private String preferredLanguage;
|
||||
|
||||
@ -64,8 +66,28 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
private int viewportWidth;
|
||||
private int viewportHeight;
|
||||
|
||||
/**
|
||||
* Constructs an instance that does not support adaptive video.
|
||||
*
|
||||
* @param eventHandler A handler to use when delivering events to listeners. May be null if
|
||||
* listeners will not be added.
|
||||
*/
|
||||
public DefaultTrackSelector(Handler eventHandler) {
|
||||
this(eventHandler, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance that uses a factory to create adaptive video track selections.
|
||||
*
|
||||
* @param eventHandler A handler to use when delivering events to listeners. May be null if
|
||||
* listeners will not be added.
|
||||
* @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s,
|
||||
* or null if the selector should not support adaptive video.
|
||||
*/
|
||||
public DefaultTrackSelector(Handler eventHandler,
|
||||
TrackSelection.Factory adaptiveVideoTrackSelectionFactory) {
|
||||
super(eventHandler);
|
||||
this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory;
|
||||
allowNonSeamlessAdaptiveness = true;
|
||||
exceedVideoConstraintsIfNecessary = true;
|
||||
maxVideoWidth = Integer.MAX_VALUE;
|
||||
@ -203,11 +225,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
rendererTrackSelections[i] = selectTrackForVideoRenderer(rendererCapabilities[i],
|
||||
rendererTrackGroupArrays[i], rendererFormatSupports[i], maxVideoWidth, maxVideoHeight,
|
||||
allowNonSeamlessAdaptiveness, allowMixedMimeAdaptiveness, viewportWidth,
|
||||
viewportHeight, orientationMayChange);
|
||||
if (rendererTrackSelections[i] == null && exceedVideoConstraintsIfNecessary) {
|
||||
rendererTrackSelections[i] = selectSmallestSupportedVideoTrack(
|
||||
rendererTrackGroupArrays[i], rendererFormatSupports[i]);
|
||||
}
|
||||
viewportHeight, orientationMayChange, adaptiveVideoTrackSelectionFactory,
|
||||
exceedVideoConstraintsIfNecessary);
|
||||
break;
|
||||
case C.TRACK_TYPE_AUDIO:
|
||||
rendererTrackSelections[i] = selectTrackForAudioRenderer(rendererTrackGroupArrays[i],
|
||||
@ -232,28 +251,35 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
RendererCapabilities rendererCapabilities, TrackGroupArray trackGroups,
|
||||
int[][] formatSupport, int maxVideoWidth, int maxVideoHeight,
|
||||
boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth,
|
||||
int viewportHeight, boolean orientationMayChange) throws ExoPlaybackException {
|
||||
int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness
|
||||
? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS)
|
||||
: RendererCapabilities.ADAPTIVE_SEAMLESS;
|
||||
boolean allowMixedMimeTypes = allowMixedMimeAdaptiveness
|
||||
&& (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport) != 0;
|
||||
TrackGroup largestAdaptiveGroup = null;
|
||||
int[] largestAdaptiveGroupTracks = NO_TRACKS;
|
||||
for (int i = 0; i < trackGroups.length; i++) {
|
||||
TrackGroup trackGroup = trackGroups.get(i);
|
||||
int[] adaptiveTracks = getAdaptiveTracksOfGroup(trackGroup, formatSupport[i],
|
||||
allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight,
|
||||
viewportWidth, viewportHeight, orientationMayChange);
|
||||
if (adaptiveTracks.length > largestAdaptiveGroupTracks.length) {
|
||||
largestAdaptiveGroup = trackGroup;
|
||||
largestAdaptiveGroupTracks = adaptiveTracks;
|
||||
int viewportHeight, boolean orientationMayChange,
|
||||
TrackSelection.Factory adaptiveVideoTrackSelectionFactory,
|
||||
boolean exceedVideoConstraintsIfNecessary) throws ExoPlaybackException {
|
||||
if (adaptiveVideoTrackSelectionFactory != null) {
|
||||
int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness
|
||||
? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS)
|
||||
: RendererCapabilities.ADAPTIVE_SEAMLESS;
|
||||
boolean allowMixedMimeTypes = allowMixedMimeAdaptiveness
|
||||
&& (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport)
|
||||
!= 0;
|
||||
TrackGroup largestAdaptiveGroup = null;
|
||||
int[] largestAdaptiveGroupTracks = NO_TRACKS;
|
||||
for (int i = 0; i < trackGroups.length; i++) {
|
||||
TrackGroup trackGroup = trackGroups.get(i);
|
||||
int[] adaptiveTracks = getAdaptiveTracksOfGroup(trackGroup, formatSupport[i],
|
||||
allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight,
|
||||
viewportWidth, viewportHeight, orientationMayChange);
|
||||
if (adaptiveTracks.length > largestAdaptiveGroupTracks.length) {
|
||||
largestAdaptiveGroup = trackGroup;
|
||||
largestAdaptiveGroupTracks = adaptiveTracks;
|
||||
}
|
||||
}
|
||||
if (largestAdaptiveGroup != null) {
|
||||
return adaptiveVideoTrackSelectionFactory.createTrackSelection(largestAdaptiveGroup,
|
||||
largestAdaptiveGroupTracks);
|
||||
}
|
||||
}
|
||||
if (largestAdaptiveGroup != null) {
|
||||
return new TrackSelection(largestAdaptiveGroup, largestAdaptiveGroupTracks);
|
||||
}
|
||||
|
||||
// TODO: Should select the best supported video track, not the first one.
|
||||
// No adaptive tracks selection could be made, so we select the first supported video track.
|
||||
for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
|
||||
TrackGroup trackGroup = trackGroups.get(groupIndex);
|
||||
@ -261,10 +287,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
|
||||
if (isSupportedVideoTrack(trackFormatSupport[trackIndex], trackGroup.getFormat(trackIndex),
|
||||
maxVideoWidth, maxVideoHeight)) {
|
||||
return new TrackSelection(trackGroup, trackIndex);
|
||||
return new FixedTrackSelection(trackGroup, trackIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exceedVideoConstraintsIfNecessary) {
|
||||
return selectSmallestSupportedVideoTrack(trackGroups, formatSupport);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -350,8 +381,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
}
|
||||
}
|
||||
}
|
||||
return trackGroupSelection != null
|
||||
? new TrackSelection(trackGroupSelection, trackIndexSelection) : null;
|
||||
return trackGroupSelection == null ? null
|
||||
: new FixedTrackSelection(trackGroupSelection, trackIndexSelection);
|
||||
}
|
||||
|
||||
private static boolean isSupportedVideoTrack(int formatSupport, Format format, int maxVideoWidth,
|
||||
@ -371,7 +402,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
|
||||
if (isSupported(trackFormatSupport[trackIndex])
|
||||
&& formatHasLanguage(trackGroup.getFormat(trackIndex), preferredLanguage)) {
|
||||
return new TrackSelection(trackGroup, trackIndex);
|
||||
return new FixedTrackSelection(trackGroup, trackIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -398,12 +429,13 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
firstForcedTrack = trackIndex;
|
||||
}
|
||||
if (formatHasLanguage(trackGroup.getFormat(trackIndex), preferredLanguage)) {
|
||||
return new TrackSelection(trackGroup, trackIndex);
|
||||
return new FixedTrackSelection(trackGroup, trackIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return firstForcedGroup != null ? new TrackSelection(firstForcedGroup, firstForcedTrack) : null;
|
||||
return firstForcedGroup == null ? null
|
||||
: new FixedTrackSelection(firstForcedGroup, firstForcedTrack);
|
||||
}
|
||||
|
||||
// General track selection methods.
|
||||
@ -415,7 +447,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
int[] trackFormatSupport = formatSupport[groupIndex];
|
||||
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
|
||||
if (isSupported(trackFormatSupport[trackIndex])) {
|
||||
return new TrackSelection(trackGroup, trackIndex);
|
||||
return new FixedTrackSelection(trackGroup, trackIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -492,7 +524,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
// Before API 25 the platform Display object does not provide a working way to identify Android
|
||||
// TVs that can show 4k resolution in a SurfaceView, so check for supported devices here.
|
||||
if (Util.SDK_INT < 25) {
|
||||
if ("Sony".equals(Util.MANUFACTURER) && Util.MODEL != null && Util.MODEL.startsWith("BRAVIA")
|
||||
if ("Sony".equals(Util.MANUFACTURER) && Util.MODEL.startsWith("BRAVIA")
|
||||
&& context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) {
|
||||
return new Point(3840, 2160);
|
||||
} else if ("NVIDIA".equals(Util.MANUFACTURER) && Util.MODEL != null
|
||||
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.trackselection;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
|
||||
/**
|
||||
* A {@link TrackSelection} consisting of a single track.
|
||||
*/
|
||||
public final class FixedTrackSelection extends BaseTrackSelection {
|
||||
|
||||
private final int reason;
|
||||
private final Object data;
|
||||
|
||||
/**
|
||||
* @param group The {@link TrackGroup}. Must not be null.
|
||||
* @param track The index of the selected track within the {@link TrackGroup}.
|
||||
*/
|
||||
public FixedTrackSelection(TrackGroup group, int track) {
|
||||
this(group, track, C.SELECTION_REASON_UNKNOWN, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param group The {@link TrackGroup}. Must not be null.
|
||||
* @param track The index of the selected track within the {@link TrackGroup}.
|
||||
* @param reason A reason for the track selection.
|
||||
* @param data Optional data associated with the track selection.
|
||||
*/
|
||||
public FixedTrackSelection(TrackGroup group, int track, int reason, Object data) {
|
||||
super(group, track);
|
||||
this.reason = reason;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSelectedTrack(long bufferedDurationUs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSelectedIndex() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSelectionReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSelectionData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.trackselection;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* A {@link TrackSelection} whose selected track is updated randomly.
|
||||
*/
|
||||
public final class RandomTrackSelection extends BaseTrackSelection {
|
||||
|
||||
private final Random random;
|
||||
|
||||
private int selectedIndex;
|
||||
|
||||
/**
|
||||
* @param group The {@link TrackGroup}. Must not be null.
|
||||
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
|
||||
* null or empty. May be in any order.
|
||||
*/
|
||||
public RandomTrackSelection(TrackGroup group, int... tracks) {
|
||||
super(group, tracks);
|
||||
random = new Random();
|
||||
selectedIndex = random.nextInt(length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param group The {@link TrackGroup}. Must not be null.
|
||||
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
|
||||
* null or empty. May be in any order.
|
||||
* @param seed A seed for the {@link Random} instance used to update the selected track.
|
||||
*/
|
||||
public RandomTrackSelection(TrackGroup group, int[] tracks, long seed) {
|
||||
super(group, tracks);
|
||||
random = new Random(seed);
|
||||
selectedIndex = random.nextInt(length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSelectedTrack(long bufferedDurationUs) {
|
||||
// Count the number of non-blacklisted formats.
|
||||
long nowMs = SystemClock.elapsedRealtime();
|
||||
int nonBlacklistedFormatCount = 0;
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (!isBlacklisted(i, nowMs)) {
|
||||
nonBlacklistedFormatCount++;
|
||||
}
|
||||
}
|
||||
|
||||
selectedIndex = random.nextInt(nonBlacklistedFormatCount);
|
||||
if (nonBlacklistedFormatCount != length) {
|
||||
// Adjust the format index to account for blacklisted formats.
|
||||
nonBlacklistedFormatCount = 0;
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (!isBlacklisted(i, nowMs) && selectedIndex == nonBlacklistedFormatCount++) {
|
||||
selectedIndex = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSelectedIndex() {
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSelectionReason() {
|
||||
return C.SELECTION_REASON_ADAPTIVE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSelectionData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -16,71 +16,64 @@
|
||||
package com.google.android.exoplayer2.trackselection;
|
||||
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.Format.DecreasingBandwidthComparator;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A track selection, consisting of a {@link TrackGroup} and a selected subset of the tracks within
|
||||
* it. The selected tracks are exposed in order of decreasing bandwidth.
|
||||
* A track selection consisting of a static subset of selected tracks belonging to a
|
||||
* {@link TrackGroup}, and a possibly varying individual selected track from the subset.
|
||||
* <p>
|
||||
* Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual selected
|
||||
* track may change as a result of calling {@link #updateSelectedTrack(long)}.
|
||||
*/
|
||||
public final class TrackSelection {
|
||||
public interface TrackSelection {
|
||||
|
||||
/**
|
||||
* The selected {@link TrackGroup}.
|
||||
* Factory for {@link TrackSelection} instances.
|
||||
*/
|
||||
public final TrackGroup group;
|
||||
/**
|
||||
* The number of selected tracks within the {@link TrackGroup}. Always greater than zero.
|
||||
*/
|
||||
public final int length;
|
||||
interface Factory {
|
||||
|
||||
private final int[] tracks;
|
||||
private final Format[] formats;
|
||||
/**
|
||||
* Creates a new selection.
|
||||
*
|
||||
* @param group The {@link TrackGroup}. Must not be null.
|
||||
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
|
||||
* null or empty. May be in any order.
|
||||
* @return The created selection.
|
||||
*/
|
||||
TrackSelection createTrackSelection(TrackGroup group, int... tracks);
|
||||
|
||||
// Lazily initialized hashcode.
|
||||
private int hashCode;
|
||||
|
||||
/**
|
||||
* @param group The {@link TrackGroup}. Must not be null.
|
||||
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
|
||||
* null or empty. May be in any order.
|
||||
*/
|
||||
public TrackSelection(TrackGroup group, int... tracks) {
|
||||
Assertions.checkState(tracks.length > 0);
|
||||
this.group = Assertions.checkNotNull(group);
|
||||
this.length = tracks.length;
|
||||
// Set the formats, sorted in order of decreasing bandwidth.
|
||||
formats = new Format[length];
|
||||
for (int i = 0; i < tracks.length; i++) {
|
||||
formats[i] = group.getFormat(tracks[i]);
|
||||
}
|
||||
Arrays.sort(formats, new DecreasingBandwidthComparator());
|
||||
// Set the format indices in the same order.
|
||||
this.tracks = new int[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
this.tracks[i] = group.indexOf(formats[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link TrackGroup} to which the selected tracks belong.
|
||||
*/
|
||||
TrackGroup getTrackGroup();
|
||||
|
||||
// Static subset of selected tracks.
|
||||
|
||||
/**
|
||||
* Returns the number of tracks in the selection.
|
||||
*/
|
||||
int length();
|
||||
|
||||
/**
|
||||
* Returns the format of the track at a given index in the selection.
|
||||
*
|
||||
* @param index The index in the selection.
|
||||
* @return The format of the selected track.
|
||||
*/
|
||||
public Format getFormat(int index) {
|
||||
return formats[index];
|
||||
}
|
||||
Format getFormat(int index);
|
||||
|
||||
/**
|
||||
* Returns a copy of the formats of the selected tracks.
|
||||
* Returns the index in the track group of the track at a given index in the selection.
|
||||
*
|
||||
* @param index The index in the selection.
|
||||
* @return The index of the selected track.
|
||||
*/
|
||||
public Format[] getFormats() {
|
||||
return formats.clone();
|
||||
}
|
||||
int getIndexInTrackGroup(int index);
|
||||
|
||||
/**
|
||||
* Returns the index in the selection of the track with the specified format.
|
||||
@ -89,66 +82,82 @@ public final class TrackSelection {
|
||||
* @return The index in the selection, or -1 if the track with the specified format is not part of
|
||||
* the selection.
|
||||
*/
|
||||
public int indexOf(Format format) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (formats[i] == format) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index in the track group of the track at a given index in the selection.
|
||||
*
|
||||
* @param index The index in the selection.
|
||||
* @return The index of the selected track.
|
||||
*/
|
||||
public int getTrack(int index) {
|
||||
return tracks[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the selected tracks in the track group.
|
||||
*/
|
||||
public int[] getTracks() {
|
||||
return tracks.clone();
|
||||
}
|
||||
int indexOf(Format format);
|
||||
|
||||
/**
|
||||
* Returns the index in the selection of the track with the specified index in the track group.
|
||||
*
|
||||
* @param trackIndex The index in the track group.
|
||||
* @param indexInTrackGroup The index in the track group.
|
||||
* @return The index in the selection, or -1 if the track with the specified index is not part of
|
||||
* the selection.
|
||||
*/
|
||||
public int indexOf(int trackIndex) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (tracks[i] == trackIndex) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
int indexOf(int indexInTrackGroup);
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (hashCode == 0) {
|
||||
hashCode = 31 * System.identityHashCode(group) + Arrays.hashCode(tracks);
|
||||
}
|
||||
return hashCode;
|
||||
}
|
||||
// Individual selected track.
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
TrackSelection other = (TrackSelection) obj;
|
||||
return group == other.group && Arrays.equals(tracks, other.tracks);
|
||||
}
|
||||
/**
|
||||
* Returns the {@link Format} of the individual selected track.
|
||||
*/
|
||||
Format getSelectedFormat();
|
||||
|
||||
/**
|
||||
* Returns the index in the track group of the individual selected track.
|
||||
*/
|
||||
int getSelectedIndexInTrackGroup();
|
||||
|
||||
/**
|
||||
* Returns the index of the selected track.
|
||||
*/
|
||||
int getSelectedIndex();
|
||||
|
||||
/**
|
||||
* Returns the reason for the current track selection.
|
||||
*/
|
||||
int getSelectionReason();
|
||||
|
||||
/**
|
||||
* Returns optional data associated with the current track selection.
|
||||
*/
|
||||
Object getSelectionData();
|
||||
|
||||
// Adaptation.
|
||||
|
||||
/**
|
||||
* Updates the selected track.
|
||||
*
|
||||
* @param bufferedDurationUs The duration of media currently buffered in microseconds.
|
||||
*/
|
||||
void updateSelectedTrack(long bufferedDurationUs);
|
||||
|
||||
/**
|
||||
* May be called periodically by sources that load media in discrete {@link MediaChunk}s and
|
||||
* support discarding of buffered chunks in order to re-buffer using a different selected track.
|
||||
* Returns the number of chunks that should be retained in the queue.
|
||||
* <p>
|
||||
* To avoid excessive re-buffering, implementations should normally return the size of the queue.
|
||||
* An example of a case where a smaller value may be returned is if network conditions have
|
||||
* improved dramatically, allowing chunks to be discarded and re-buffered in a track of
|
||||
* significantly higher quality. Discarding chunks may allow faster switching to a higher quality
|
||||
* track in this case.
|
||||
*
|
||||
* @param playbackPositionUs The current playback position in microseconds.
|
||||
* @param queue The queue of buffered {@link MediaChunk}s. Must not be modified.
|
||||
* @return The number of chunks to retain in the queue.
|
||||
*/
|
||||
int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue);
|
||||
|
||||
/**
|
||||
* Attempts to blacklist the track at the specified index in the selection, making it ineligible
|
||||
* for selection by calls to {@link #updateSelectedTrack(long)} for the specified period of time.
|
||||
* Blacklisting will fail if all other tracks are currently blacklisted. If blacklisting the
|
||||
* currently selected track, note that it will remain selected until the next call to
|
||||
* {@link #updateSelectedTrack(long)}.
|
||||
*
|
||||
* @param index The index of the track in the selection.
|
||||
* @param blacklistDurationMs The duration of time for which the track should be blacklisted, in
|
||||
* milliseconds.
|
||||
* @return Whether blacklisting was successful.
|
||||
*/
|
||||
boolean blacklist(int index, long blacklistDurationMs);
|
||||
|
||||
}
|
||||
|
@ -34,17 +34,18 @@ import com.google.android.exoplayer2.playbacktests.util.MetricsLogger;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
@ -680,15 +681,16 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
|
||||
this.isWidevineEncrypted = isWidevineEncrypted;
|
||||
this.videoMimeType = videoMimeType;
|
||||
this.isCddLimitedRetry = isCddLimitedRetry;
|
||||
trackSelector = new DashTestTrackSelector(new String[] {audioFormat},
|
||||
videoFormats, canIncludeAdditionalVideoFormats);
|
||||
trackSelector = new DashTestTrackSelector(audioFormat, videoFormats,
|
||||
canIncludeAdditionalVideoFormats);
|
||||
if (actionSchedule != null) {
|
||||
setSchedule(actionSchedule);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MappingTrackSelector buildTrackSelector(HostActivity host) {
|
||||
protected MappingTrackSelector buildTrackSelector(HostActivity host,
|
||||
BandwidthMeter bandwidthMeter) {
|
||||
return trackSelector;
|
||||
}
|
||||
|
||||
@ -731,19 +733,17 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaSource buildSource(HostActivity host, String userAgent) {
|
||||
public MediaSource buildSource(HostActivity host, String userAgent,
|
||||
TransferListener mediaTransferListener) {
|
||||
DataSource.Factory manifestDataSourceFactory = new DefaultDataSourceFactory(host, userAgent);
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
DataSource.Factory mediaDataSourceFactory = new DefaultDataSourceFactory(host, userAgent,
|
||||
bandwidthMeter);
|
||||
FormatEvaluator.Factory formatEvaluatorFactory = new AdaptiveEvaluator.Factory(
|
||||
bandwidthMeter);
|
||||
mediaTransferListener);
|
||||
String manifestUrl = manifestPath;
|
||||
manifestUrl += isWidevineEncrypted ? (needsSecureVideoDecoder ? WIDEVINE_L1_SUFFIX
|
||||
: WIDEVINE_L3_SUFFIX) : "";
|
||||
Uri manifestUri = Uri.parse(manifestUrl);
|
||||
DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory(
|
||||
mediaDataSourceFactory, formatEvaluatorFactory);
|
||||
mediaDataSourceFactory);
|
||||
return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory,
|
||||
MIN_LOADABLE_RETRY_COUNT, null, null);
|
||||
}
|
||||
@ -801,16 +801,16 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
|
||||
|
||||
private static final class DashTestTrackSelector extends MappingTrackSelector {
|
||||
|
||||
private final String[] audioFormatIds;
|
||||
private final String audioFormatId;
|
||||
private final String[] videoFormatIds;
|
||||
private final boolean canIncludeAdditionalVideoFormats;
|
||||
|
||||
public boolean includedAdditionalVideoFormats;
|
||||
|
||||
private DashTestTrackSelector(String[] audioFormatIds, String[] videoFormatIds,
|
||||
private DashTestTrackSelector(String audioFormatId, String[] videoFormatIds,
|
||||
boolean canIncludeAdditionalVideoFormats) {
|
||||
super(null);
|
||||
this.audioFormatIds = audioFormatIds;
|
||||
this.audioFormatId = audioFormatId;
|
||||
this.videoFormatIds = videoFormatIds;
|
||||
this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats;
|
||||
}
|
||||
@ -826,17 +826,17 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
|
||||
Assertions.checkState(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].length == 1);
|
||||
Assertions.checkState(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].length == 1);
|
||||
TrackSelection[] selections = new TrackSelection[rendererCapabilities.length];
|
||||
selections[VIDEO_RENDERER_INDEX] = new TrackSelection(
|
||||
selections[VIDEO_RENDERER_INDEX] = new RandomTrackSelection(
|
||||
rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0),
|
||||
getTrackIndices(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0),
|
||||
rendererFormatSupports[VIDEO_RENDERER_INDEX][0], videoFormatIds,
|
||||
canIncludeAdditionalVideoFormats));
|
||||
selections[AUDIO_RENDERER_INDEX] = new TrackSelection(
|
||||
canIncludeAdditionalVideoFormats),
|
||||
0 /* seed */);
|
||||
selections[AUDIO_RENDERER_INDEX] = new FixedTrackSelection(
|
||||
rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0),
|
||||
getTrackIndices(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0),
|
||||
rendererFormatSupports[AUDIO_RENDERER_INDEX][0], audioFormatIds, false));
|
||||
getTrackIndex(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), audioFormatId));
|
||||
includedAdditionalVideoFormats =
|
||||
selections[VIDEO_RENDERER_INDEX].length > videoFormatIds.length;
|
||||
selections[VIDEO_RENDERER_INDEX].length() > videoFormatIds.length;
|
||||
return selections;
|
||||
}
|
||||
|
||||
@ -844,18 +844,9 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
|
||||
String[] formatIds, boolean canIncludeAdditionalFormats) {
|
||||
List<Integer> trackIndices = new ArrayList<>();
|
||||
|
||||
// Always select explicitly listed representations, failing if they're missing.
|
||||
// Always select explicitly listed representations.
|
||||
for (String formatId : formatIds) {
|
||||
boolean foundIndex = false;
|
||||
for (int j = 0; j < trackGroup.length && !foundIndex; j++) {
|
||||
if (trackGroup.getFormat(j).id.equals(formatId)) {
|
||||
trackIndices.add(j);
|
||||
foundIndex = true;
|
||||
}
|
||||
}
|
||||
if (!foundIndex) {
|
||||
throw new IllegalStateException("Format " + formatId + " not found.");
|
||||
}
|
||||
trackIndices.add(getTrackIndex(trackGroup, formatId));
|
||||
}
|
||||
|
||||
// Select additional video representations, if supported by the device.
|
||||
@ -873,6 +864,15 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
|
||||
return trackIndicesArray;
|
||||
}
|
||||
|
||||
private static int getTrackIndex(TrackGroup trackGroup, String formatId) {
|
||||
for (int i = 0; i < trackGroup.length; i++) {
|
||||
if (trackGroup.getFormat(i).id.equals(formatId)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Format " + formatId + " not found.");
|
||||
}
|
||||
|
||||
private static boolean isFormatHandled(int formatSupport) {
|
||||
return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK)
|
||||
== RendererCapabilities.FORMAT_HANDLED;
|
||||
|
@ -27,15 +27,18 @@ import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.Timeline;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
|
||||
@ -123,11 +126,12 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen
|
||||
@Override
|
||||
public final void onStart(HostActivity host, Surface surface) {
|
||||
// Build the player.
|
||||
trackSelector = buildTrackSelector(host);
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
trackSelector = buildTrackSelector(host, bandwidthMeter);
|
||||
String userAgent = "ExoPlayerPlaybackTests";
|
||||
DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent);
|
||||
player = buildExoPlayer(host, surface, trackSelector, drmSessionManager);
|
||||
player.setMediaSource(buildSource(host, Util.getUserAgent(host, userAgent)));
|
||||
player.setMediaSource(buildSource(host, Util.getUserAgent(host, userAgent), bandwidthMeter));
|
||||
player.addListener(this);
|
||||
player.setDebugListener(this);
|
||||
player.setPlayWhenReady(true);
|
||||
@ -288,8 +292,9 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected MappingTrackSelector buildTrackSelector(HostActivity host) {
|
||||
return new DefaultTrackSelector(null);
|
||||
protected MappingTrackSelector buildTrackSelector(HostActivity host,
|
||||
BandwidthMeter bandwidthMeter) {
|
||||
return new DefaultTrackSelector(null, new AdaptiveVideoTrackSelection.Factory(bandwidthMeter));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ -302,7 +307,8 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected abstract MediaSource buildSource(HostActivity host, String userAgent);
|
||||
protected abstract MediaSource buildSource(HostActivity host, String userAgent,
|
||||
TransferListener mediaTransferListener);
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected void onPlayerErrorInternal(ExoPlaybackException error) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user