diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java index 0f88525b68..7b2420add9 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java @@ -28,8 +28,10 @@ public interface FormatEvaluator { /** * Enables the evaluator. + * + * @param formats The formats from which to select, ordered by decreasing bandwidth. */ - void enable(); + void enable(Format[] formats); /** * Disables the evaluator. @@ -39,22 +41,27 @@ public interface FormatEvaluator { /** * Update the supplied evaluation. *
- * When the method is invoked, {@code evaluation} will contain the currently selected - * format (null for the first evaluation), the most recent trigger (TRIGGER_INITIAL for the - * first evaluation) and the current queue size. The implementation should update these - * fields as necessary. - *
- * The trigger should be considered "sticky" for as long as a given representation is selected,
- * and so should only be changed if the representation is also changed.
+ * When invoked, {@code evaluation} must contain the currently selected format (null for an
+ * initial evaluation), the most recent trigger (@link Chunk#TRIGGER_INITIAL} for an initial
+ * evaluation) and the size of {@code queue}. The invocation will update the format and trigger,
+ * and may also reduce {@link Evaluation#queueSize} to indicate that chunks should be discarded
+ * from the end of the queue to allow re-buffering in a different format. The evaluation will
+ * always retain the first chunk in the queue, if there is one.
*
- * @param queue A read only representation of the currently buffered {@link MediaChunk}s.
- * @param playbackPositionUs The current playback position.
- * @param formats The formats from which to select, ordered by decreasing bandwidth.
- * @param evaluation The evaluation.
+ * @param queue A read only representation of currently buffered chunks. Must not be empty unless
+ * the evaluation is at the start of playback or immediately follows a seek. All but the first
+ * chunk may be discarded. A caller may pass a singleton list containing only the most
+ * recently buffered chunk in the case that it does not support discarding of chunks.
+ * @param playbackPositionUs The current playback position in microseconds.
+ * @param switchingOverlapUs If switching format requires downloading overlapping media then this
+ * is the duration of the required overlap in microseconds. 0 otherwise.
+ * @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.
*/
- // TODO: Pass more useful information into this method, and finalize the interface.
- void evaluate(List extends MediaChunk> queue, long playbackPositionUs, Format[] formats,
- Evaluation evaluation);
+ void evaluate(List extends MediaChunk> queue, long playbackPositionUs,
+ long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation);
/**
* A format evaluation.
@@ -62,9 +69,9 @@ public interface FormatEvaluator {
public static final class Evaluation {
/**
- * The desired size of the queue.
+ * The selected format.
*/
- public int queueSize;
+ public Format format;
/**
* The sticky reason for the format selection.
@@ -72,46 +79,33 @@ public interface FormatEvaluator {
public int trigger;
/**
- * The selected format.
+ * The desired size of the queue.
*/
- public Format format;
+ public int queueSize;
public Evaluation() {
trigger = Chunk.TRIGGER_INITIAL;
}
+ /**
+ * Clears {@link #format} and sets {@link #trigger} to {@link Chunk#TRIGGER_INITIAL}.
+ */
+ public void clear() {
+ format = null;
+ trigger = Chunk.TRIGGER_INITIAL;
+ }
+
}
/**
- * Always selects the first format.
- */
- public static final class FixedEvaluator implements FormatEvaluator {
-
- @Override
- public void enable() {
- // Do nothing.
- }
-
- @Override
- public void disable() {
- // Do nothing.
- }
-
- @Override
- public void evaluate(List extends MediaChunk> queue, long playbackPositionUs,
- Format[] formats, Evaluation evaluation) {
- evaluation.format = formats[0];
- }
-
- }
-
- /**
- * Selects randomly between the available formats.
+ * Selects randomly between the available formats, excluding those that are blacklisted.
*/
public static final class RandomEvaluator implements FormatEvaluator {
private final Random random;
+ private Format[] formats;
+
public RandomEvaluator() {
this.random = new Random();
}
@@ -124,20 +118,39 @@ public interface FormatEvaluator {
}
@Override
- public void enable() {
- // Do nothing.
+ public void enable(Format[] formats) {
+ this.formats = formats;
}
@Override
public void disable() {
- // Do nothing.
+ formats = null;
}
@Override
public void evaluate(List extends MediaChunk> queue, long playbackPositionUs,
- Format[] formats, Evaluation evaluation) {
- Format newFormat = formats[random.nextInt(formats.length)];
- if (evaluation.format != null && !evaluation.format.equals(newFormat)) {
+ long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation) {
+ // Count the number of non-blacklisted formats.
+ int nonBlacklistedFormatCount = 0;
+ for (int i = 0; i < blacklistFlags.length; i++) {
+ if (!blacklistFlags[i]) {
+ 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.trigger = Chunk.TRIGGER_ADAPTIVE;
}
evaluation.format = newFormat;
@@ -163,13 +176,14 @@ public interface FormatEvaluator {
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 Format[] formats;
+
/**
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
*/
@@ -211,22 +225,26 @@ public interface FormatEvaluator {
}
@Override
- public void enable() {
- // Do nothing.
+ public void enable(Format[] formats) {
+ this.formats = formats;
}
@Override
public void disable() {
- // Do nothing.
+ formats = null;
}
@Override
public void evaluate(List extends MediaChunk> queue, long playbackPositionUs,
- Format[] formats, Evaluation evaluation) {
+ long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation) {
long bufferedDurationUs = queue.isEmpty() ? 0
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
+ if (switchingOverlapUs > 0) {
+ bufferedDurationUs = Math.max(0, bufferedDurationUs - switchingOverlapUs);
+ }
Format current = evaluation.format;
- Format ideal = determineIdealFormat(formats, bandwidthMeter.getBitrateEstimate());
+ Format ideal = determineIdealFormat(formats, blacklistFlags,
+ bandwidthMeter.getBitrateEstimate());
boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate;
boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate;
if (isHigher) {
@@ -267,17 +285,22 @@ public interface FormatEvaluator {
/**
* Compute the ideal format ignoring buffer health.
*/
- private Format determineIdealFormat(Format[] formats, long bitrateEstimate) {
+ 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 (format.bitrate <= effectiveBitrate) {
- return format;
+ if (!blacklistFlags[i]) {
+ if (format.bitrate <= effectiveBitrate) {
+ return format;
+ } else {
+ lowestBitrateNonBlacklistedIndex = i;
+ }
}
}
- // We didn't manage to calculate a suitable format. Return the lowest quality format.
- return formats[formats.length - 1];
+ return formats[lowestBitrateNonBlacklistedIndex];
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java
index c4e98f32ec..0a78498d5f 100644
--- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java
@@ -131,6 +131,7 @@ public class DashChunkSource implements ChunkSource {
// Properties of enabled tracks.
private Format[] enabledFormats;
+ private boolean[] adaptiveFormatBlacklistFlags;
/**
* @param manifestFetcher A fetcher for the manifest.
@@ -266,7 +267,8 @@ public class DashChunkSource implements ChunkSource {
}
Arrays.sort(enabledFormats, new DecreasingBandwidthComparator());
if (enabledFormats.length > 1) {
- adaptiveFormatEvaluator.enable();
+ adaptiveFormatEvaluator.enable(enabledFormats);
+ adaptiveFormatBlacklistFlags = new boolean[tracks.length];
}
processManifest(manifestFetcher.getManifest());
}
@@ -311,7 +313,8 @@ public class DashChunkSource implements ChunkSource {
evaluation.queueSize = queue.size();
if (evaluation.format == null || !lastChunkWasInitialization) {
if (enabledFormats.length > 1) {
- adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, enabledFormats, evaluation);
+ adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, 0, adaptiveFormatBlacklistFlags,
+ evaluation);
} else {
evaluation.format = enabledFormats[0];
evaluation.trigger = Chunk.TRIGGER_MANUAL;
@@ -479,7 +482,7 @@ public class DashChunkSource implements ChunkSource {
adaptiveFormatEvaluator.disable();
}
periodHolders.clear();
- evaluation.format = null;
+ evaluation.clear();
availableRange = null;
fatalError = null;
enabledFormats = null;
diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java
index 3a9c4753a7..0f3c11c156 100644
--- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java
@@ -22,6 +22,8 @@ import com.google.android.exoplayer.chunk.BaseChunkSampleSourceEventListener;
import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkOperationHolder;
import com.google.android.exoplayer.chunk.DataChunk;
+import com.google.android.exoplayer.chunk.FormatEvaluator;
+import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.mp3.Mp3Extractor;
import com.google.android.exoplayer.extractor.ts.AdtsExtractor;
@@ -31,7 +33,6 @@ import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException;
-import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.UriUtil;
@@ -99,18 +100,6 @@ public class HlsChunkSource {
*/
public static final int ADAPTIVE_MODE_ABRUPT = 3;
- /**
- * The default minimum duration of media that needs to be buffered for a switch to a higher
- * quality variant to be considered.
- */
- public static final long DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS = 5000;
-
- /**
- * The default maximum duration of media that needs to be buffered for a switch to a lower
- * quality variant to be considered.
- */
- public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000;
-
/**
* The default time for which a media playlist should be blacklisted.
*/
@@ -121,19 +110,16 @@ public class HlsChunkSource {
private static final String MP3_FILE_EXTENSION = ".mp3";
private static final String VTT_FILE_EXTENSION = ".vtt";
private static final String WEBVTT_FILE_EXTENSION = ".webvtt";
- private static final float BANDWIDTH_FRACTION = 0.8f;
private final ManifestFetcher
* This method should only be called after the source has been prepared.
*
* @param tracks The track indices.
*/
public void selectTracks(int[] tracks) {
+ evaluation.clear();
enabledVariants = new Variant[tracks.length];
enabledVariantPlaylists = new HlsMediaPlaylist[enabledVariants.length];
enabledVariantLastPlaylistLoadTimesMs = new long[enabledVariants.length];
enabledVariantBlacklistTimes = new long[enabledVariants.length];
+ enabledVariantBlacklistFlags = new boolean[enabledVariants.length];
// Construct and sort the enabled variants.
for (int i = 0; i < tracks.length; i++) {
enabledVariants[i] = exposedVariants[tracks[i]];
@@ -327,15 +290,13 @@ public class HlsChunkSource {
return formatComparator.compare(first.format, second.format);
}
});
- // Determine the initial variant index and maximum video dimensions.
- selectedVariantIndex = 0;
- int minOriginalVariantIndex = Integer.MAX_VALUE;
- for (int i = 0; i < enabledVariants.length; i++) {
- int originalVariantIndex = masterPlaylist.variants.indexOf(enabledVariants[i]);
- if (originalVariantIndex < minOriginalVariantIndex) {
- minOriginalVariantIndex = originalVariantIndex;
- selectedVariantIndex = i;
+ if (enabledVariants.length > 1) {
+ // TODO[REFACTOR]: We need to disable this at some point.
+ Format[] formats = new Format[enabledVariants.length];
+ for (int i = 0; i < formats.length; i++) {
+ formats[i] = enabledVariants[i].format;
}
+ adaptiveFormatEvaluator.enable(formats);
}
}
@@ -560,7 +521,8 @@ public class HlsChunkSource {
EncryptionKeyChunk encryptionChunk = (EncryptionKeyChunk) chunk;
enabledVariantIndex = encryptionChunk.variantIndex;
}
- boolean alreadyBlacklisted = enabledVariantBlacklistTimes[enabledVariantIndex] != 0;
+ boolean alreadyBlacklisted = enabledVariantBlacklistFlags[enabledVariantIndex];
+ enabledVariantBlacklistFlags[enabledVariantIndex] = true;
enabledVariantBlacklistTimes[enabledVariantIndex] = SystemClock.elapsedRealtime();
if (alreadyBlacklisted) {
// The playlist was already blacklisted.
@@ -576,7 +538,7 @@ public class HlsChunkSource {
// This was the last non-blacklisted playlist. Don't blacklist it.
Log.w(TAG, "Final variant not blacklisted (" + responseCode + "): "
+ chunk.dataSpec.uri);
- enabledVariantBlacklistTimes[enabledVariantIndex] = 0;
+ enabledVariantBlacklistFlags[enabledVariantIndex] = false;
return false;
}
}
@@ -644,57 +606,29 @@ public class HlsChunkSource {
private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) {
clearStaleBlacklistedVariants();
- long bitrateEstimate = bandwidthMeter.getBitrateEstimate();
- if (enabledVariantBlacklistTimes[selectedVariantIndex] != 0) {
- // The current variant has been blacklisted, so we have no choice but to re-evaluate.
- return getVariantIndexForBandwidth(bitrateEstimate);
+ long switchingOverlapUs;
+ List