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:
olly 2016-07-28 09:33:15 -07:00 committed by Oliver Woodman
parent 5eb6190682
commit 3501332dd3
25 changed files with 1048 additions and 866 deletions

View File

@ -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) {

View File

@ -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);

View File

@ -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) {

View File

@ -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>

View File

@ -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,

View File

@ -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;

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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;
}
}
}

View File

@ -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,

View File

@ -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() {

View File

@ -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;

View File

@ -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);
}

View File

@ -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();

View File

@ -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,

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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) {