Use FormatEvaluator for HLS.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=114743795
This commit is contained in:
olly 2016-02-16 05:18:47 -08:00 committed by Oliver Woodman
parent a7adcce018
commit c48dd4f3e3
5 changed files with 165 additions and 192 deletions

View File

@ -28,8 +28,10 @@ public interface FormatEvaluator {
/** /**
* Enables the evaluator. * Enables the evaluator.
*
* @param formats The formats from which to select, ordered by decreasing bandwidth.
*/ */
void enable(); void enable(Format[] formats);
/** /**
* Disables the evaluator. * Disables the evaluator.
@ -39,22 +41,27 @@ public interface FormatEvaluator {
/** /**
* Update the supplied evaluation. * Update the supplied evaluation.
* <p> * <p>
* When the method is invoked, {@code evaluation} will contain the currently selected * When invoked, {@code evaluation} must contain the currently selected format (null for an
* format (null for the first evaluation), the most recent trigger (TRIGGER_INITIAL for the * initial evaluation), the most recent trigger (@link Chunk#TRIGGER_INITIAL} for an initial
* first evaluation) and the current queue size. The implementation should update these * evaluation) and the size of {@code queue}. The invocation will update the format and trigger,
* fields as necessary. * and may also reduce {@link Evaluation#queueSize} to indicate that chunks should be discarded
* <p> * from the end of the queue to allow re-buffering in a different format. The evaluation will
* The trigger should be considered "sticky" for as long as a given representation is selected, * always retain the first chunk in the queue, if there is one.
* and so should only be changed if the representation is also changed.
* *
* @param queue A read only representation of the currently buffered {@link MediaChunk}s. * @param queue A read only representation of currently buffered chunks. Must not be empty unless
* @param playbackPositionUs The current playback position. * the evaluation is at the start of playback or immediately follows a seek. All but the first
* @param formats The formats from which to select, ordered by decreasing bandwidth. * chunk may be discarded. A caller may pass a singleton list containing only the most
* @param evaluation The evaluation. * 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,
void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs, Format[] formats, long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation);
Evaluation evaluation);
/** /**
* A format evaluation. * A format evaluation.
@ -62,9 +69,9 @@ public interface FormatEvaluator {
public static final class Evaluation { 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. * The sticky reason for the format selection.
@ -72,46 +79,33 @@ public interface FormatEvaluator {
public int trigger; public int trigger;
/** /**
* The selected format. * The desired size of the queue.
*/ */
public Format format; public int queueSize;
public Evaluation() { public Evaluation() {
trigger = Chunk.TRIGGER_INITIAL; 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. * Selects randomly between the available formats, excluding those that are blacklisted.
*/
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.
*/ */
public static final class RandomEvaluator implements FormatEvaluator { public static final class RandomEvaluator implements FormatEvaluator {
private final Random random; private final Random random;
private Format[] formats;
public RandomEvaluator() { public RandomEvaluator() {
this.random = new Random(); this.random = new Random();
} }
@ -124,20 +118,39 @@ public interface FormatEvaluator {
} }
@Override @Override
public void enable() { public void enable(Format[] formats) {
// Do nothing. this.formats = formats;
} }
@Override @Override
public void disable() { public void disable() {
// Do nothing. formats = null;
} }
@Override @Override
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs, public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
Format[] formats, Evaluation evaluation) { long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation) {
Format newFormat = formats[random.nextInt(formats.length)]; // Count the number of non-blacklisted formats.
if (evaluation.format != null && !evaluation.format.equals(newFormat)) { 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.trigger = Chunk.TRIGGER_ADAPTIVE;
} }
evaluation.format = newFormat; evaluation.format = newFormat;
@ -163,13 +176,14 @@ public interface FormatEvaluator {
public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f;
private final BandwidthMeter bandwidthMeter; private final BandwidthMeter bandwidthMeter;
private final int maxInitialBitrate; private final int maxInitialBitrate;
private final long minDurationForQualityIncreaseUs; private final long minDurationForQualityIncreaseUs;
private final long maxDurationForQualityDecreaseUs; private final long maxDurationForQualityDecreaseUs;
private final long minDurationToRetainAfterDiscardUs; private final long minDurationToRetainAfterDiscardUs;
private final float bandwidthFraction; private final float bandwidthFraction;
private Format[] formats;
/** /**
* @param bandwidthMeter Provides an estimate of the currently available bandwidth. * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
*/ */
@ -211,22 +225,26 @@ public interface FormatEvaluator {
} }
@Override @Override
public void enable() { public void enable(Format[] formats) {
// Do nothing. this.formats = formats;
} }
@Override @Override
public void disable() { public void disable() {
// Do nothing. formats = null;
} }
@Override @Override
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs, public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
Format[] formats, Evaluation evaluation) { long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation) {
long bufferedDurationUs = queue.isEmpty() ? 0 long bufferedDurationUs = queue.isEmpty() ? 0
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs; : queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
if (switchingOverlapUs > 0) {
bufferedDurationUs = Math.max(0, bufferedDurationUs - switchingOverlapUs);
}
Format current = evaluation.format; 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 isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate;
boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate; boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate;
if (isHigher) { if (isHigher) {
@ -267,17 +285,22 @@ public interface FormatEvaluator {
/** /**
* Compute the ideal format ignoring buffer health. * 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 long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE
? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction); ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
for (int i = 0; i < formats.length; i++) { for (int i = 0; i < formats.length; i++) {
Format format = formats[i]; Format format = formats[i];
if (!blacklistFlags[i]) {
if (format.bitrate <= effectiveBitrate) { if (format.bitrate <= effectiveBitrate) {
return format; 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];
} }
} }

View File

@ -131,6 +131,7 @@ public class DashChunkSource implements ChunkSource {
// Properties of enabled tracks. // Properties of enabled tracks.
private Format[] enabledFormats; private Format[] enabledFormats;
private boolean[] adaptiveFormatBlacklistFlags;
/** /**
* @param manifestFetcher A fetcher for the manifest. * @param manifestFetcher A fetcher for the manifest.
@ -266,7 +267,8 @@ public class DashChunkSource implements ChunkSource {
} }
Arrays.sort(enabledFormats, new DecreasingBandwidthComparator()); Arrays.sort(enabledFormats, new DecreasingBandwidthComparator());
if (enabledFormats.length > 1) { if (enabledFormats.length > 1) {
adaptiveFormatEvaluator.enable(); adaptiveFormatEvaluator.enable(enabledFormats);
adaptiveFormatBlacklistFlags = new boolean[tracks.length];
} }
processManifest(manifestFetcher.getManifest()); processManifest(manifestFetcher.getManifest());
} }
@ -311,7 +313,8 @@ public class DashChunkSource implements ChunkSource {
evaluation.queueSize = queue.size(); evaluation.queueSize = queue.size();
if (evaluation.format == null || !lastChunkWasInitialization) { if (evaluation.format == null || !lastChunkWasInitialization) {
if (enabledFormats.length > 1) { if (enabledFormats.length > 1) {
adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, enabledFormats, evaluation); adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, 0, adaptiveFormatBlacklistFlags,
evaluation);
} else { } else {
evaluation.format = enabledFormats[0]; evaluation.format = enabledFormats[0];
evaluation.trigger = Chunk.TRIGGER_MANUAL; evaluation.trigger = Chunk.TRIGGER_MANUAL;
@ -479,7 +482,7 @@ public class DashChunkSource implements ChunkSource {
adaptiveFormatEvaluator.disable(); adaptiveFormatEvaluator.disable();
} }
periodHolders.clear(); periodHolders.clear();
evaluation.format = null; evaluation.clear();
availableRange = null; availableRange = null;
fatalError = null; fatalError = null;
enabledFormats = null; enabledFormats = null;

View File

@ -22,6 +22,8 @@ import com.google.android.exoplayer.chunk.BaseChunkSampleSourceEventListener;
import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.ChunkOperationHolder;
import com.google.android.exoplayer.chunk.DataChunk; 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.Extractor;
import com.google.android.exoplayer.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer.extractor.mp3.Mp3Extractor;
import com.google.android.exoplayer.extractor.ts.AdtsExtractor; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; 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.ManifestFetcher;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.UriUtil; import com.google.android.exoplayer.util.UriUtil;
@ -99,18 +100,6 @@ public class HlsChunkSource {
*/ */
public static final int ADAPTIVE_MODE_ABRUPT = 3; 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. * 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 MP3_FILE_EXTENSION = ".mp3";
private static final String VTT_FILE_EXTENSION = ".vtt"; private static final String VTT_FILE_EXTENSION = ".vtt";
private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; private static final String WEBVTT_FILE_EXTENSION = ".webvtt";
private static final float BANDWIDTH_FRACTION = 0.8f;
private final ManifestFetcher<HlsPlaylist> manifestFetcher; private final ManifestFetcher<HlsPlaylist> manifestFetcher;
private final int type; private final int type;
private final DataSource dataSource; private final DataSource dataSource;
private final FormatEvaluator adaptiveFormatEvaluator;
private final Evaluation evaluation;
private final HlsPlaylistParser playlistParser; private final HlsPlaylistParser playlistParser;
private final BandwidthMeter bandwidthMeter;
private final PtsTimestampAdjusterProvider timestampAdjusterProvider; private final PtsTimestampAdjusterProvider timestampAdjusterProvider;
private final int adaptiveMode; private final int adaptiveMode;
private final long minBufferDurationToSwitchUpUs;
private final long maxBufferDurationToSwitchDownUs;
private boolean manifestFetcherEnabled; private boolean manifestFetcherEnabled;
private byte[] scratchSpace; private byte[] scratchSpace;
private boolean live; private boolean live;
@ -155,6 +141,7 @@ public class HlsChunkSource {
private HlsMediaPlaylist[] enabledVariantPlaylists; private HlsMediaPlaylist[] enabledVariantPlaylists;
private long[] enabledVariantLastPlaylistLoadTimesMs; private long[] enabledVariantLastPlaylistLoadTimesMs;
private long[] enabledVariantBlacklistTimes; private long[] enabledVariantBlacklistTimes;
private boolean[] enabledVariantBlacklistFlags;
private int selectedVariantIndex; private int selectedVariantIndex;
/** /**
@ -173,40 +160,14 @@ public class HlsChunkSource {
public HlsChunkSource(ManifestFetcher<HlsPlaylist> manifestFetcher, int type, public HlsChunkSource(ManifestFetcher<HlsPlaylist> manifestFetcher, int type,
DataSource dataSource, BandwidthMeter bandwidthMeter, DataSource dataSource, BandwidthMeter bandwidthMeter,
PtsTimestampAdjusterProvider timestampAdjusterProvider, int adaptiveMode) { PtsTimestampAdjusterProvider timestampAdjusterProvider, int adaptiveMode) {
this(manifestFetcher, type, dataSource, bandwidthMeter, timestampAdjusterProvider,
adaptiveMode, DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS);
}
/**
* @param manifestFetcher A fetcher for the playlist.
* @param type The type of chunk provided by the source. One of {@link #TYPE_DEFAULT} and
* {@link #TYPE_VTT}.
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
* @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 adaptiveMode The mode for switching from one variant to another. One of
* {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and
* {@link #ADAPTIVE_MODE_SPLICE}.
* @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered
* for a switch to a higher quality variant to be considered.
* @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered
* for a switch to a lower quality variant to be considered.
*/
public HlsChunkSource(ManifestFetcher<HlsPlaylist> manifestFetcher, int type,
DataSource dataSource, BandwidthMeter bandwidthMeter,
PtsTimestampAdjusterProvider timestampAdjusterProvider, int adaptiveMode,
long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs) {
this.manifestFetcher = manifestFetcher; this.manifestFetcher = manifestFetcher;
this.type = type; this.type = type;
this.dataSource = dataSource; this.dataSource = dataSource;
this.bandwidthMeter = bandwidthMeter; this.adaptiveFormatEvaluator = new FormatEvaluator.AdaptiveEvaluator(bandwidthMeter);
this.timestampAdjusterProvider = timestampAdjusterProvider; this.timestampAdjusterProvider = timestampAdjusterProvider;
this.adaptiveMode = adaptiveMode; this.adaptiveMode = adaptiveMode;
minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000;
maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000;
playlistParser = new HlsPlaylistParser(); playlistParser = new HlsPlaylistParser();
evaluation = new Evaluation();
} }
/** /**
@ -304,17 +265,19 @@ public class HlsChunkSource {
} }
/** /**
* Selects a tracks for use. * Selects tracks for use.
* <p> * <p>
* This method should only be called after the source has been prepared. * This method should only be called after the source has been prepared.
* *
* @param tracks The track indices. * @param tracks The track indices.
*/ */
public void selectTracks(int[] tracks) { public void selectTracks(int[] tracks) {
evaluation.clear();
enabledVariants = new Variant[tracks.length]; enabledVariants = new Variant[tracks.length];
enabledVariantPlaylists = new HlsMediaPlaylist[enabledVariants.length]; enabledVariantPlaylists = new HlsMediaPlaylist[enabledVariants.length];
enabledVariantLastPlaylistLoadTimesMs = new long[enabledVariants.length]; enabledVariantLastPlaylistLoadTimesMs = new long[enabledVariants.length];
enabledVariantBlacklistTimes = new long[enabledVariants.length]; enabledVariantBlacklistTimes = new long[enabledVariants.length];
enabledVariantBlacklistFlags = new boolean[enabledVariants.length];
// Construct and sort the enabled variants. // Construct and sort the enabled variants.
for (int i = 0; i < tracks.length; i++) { for (int i = 0; i < tracks.length; i++) {
enabledVariants[i] = exposedVariants[tracks[i]]; enabledVariants[i] = exposedVariants[tracks[i]];
@ -327,15 +290,13 @@ public class HlsChunkSource {
return formatComparator.compare(first.format, second.format); return formatComparator.compare(first.format, second.format);
} }
}); });
// Determine the initial variant index and maximum video dimensions. if (enabledVariants.length > 1) {
selectedVariantIndex = 0; // TODO[REFACTOR]: We need to disable this at some point.
int minOriginalVariantIndex = Integer.MAX_VALUE; Format[] formats = new Format[enabledVariants.length];
for (int i = 0; i < enabledVariants.length; i++) { for (int i = 0; i < formats.length; i++) {
int originalVariantIndex = masterPlaylist.variants.indexOf(enabledVariants[i]); formats[i] = enabledVariants[i].format;
if (originalVariantIndex < minOriginalVariantIndex) {
minOriginalVariantIndex = originalVariantIndex;
selectedVariantIndex = i;
} }
adaptiveFormatEvaluator.enable(formats);
} }
} }
@ -560,7 +521,8 @@ public class HlsChunkSource {
EncryptionKeyChunk encryptionChunk = (EncryptionKeyChunk) chunk; EncryptionKeyChunk encryptionChunk = (EncryptionKeyChunk) chunk;
enabledVariantIndex = encryptionChunk.variantIndex; enabledVariantIndex = encryptionChunk.variantIndex;
} }
boolean alreadyBlacklisted = enabledVariantBlacklistTimes[enabledVariantIndex] != 0; boolean alreadyBlacklisted = enabledVariantBlacklistFlags[enabledVariantIndex];
enabledVariantBlacklistFlags[enabledVariantIndex] = true;
enabledVariantBlacklistTimes[enabledVariantIndex] = SystemClock.elapsedRealtime(); enabledVariantBlacklistTimes[enabledVariantIndex] = SystemClock.elapsedRealtime();
if (alreadyBlacklisted) { if (alreadyBlacklisted) {
// The playlist was already blacklisted. // The playlist was already blacklisted.
@ -576,7 +538,7 @@ public class HlsChunkSource {
// This was the last non-blacklisted playlist. Don't blacklist it. // This was the last non-blacklisted playlist. Don't blacklist it.
Log.w(TAG, "Final variant not blacklisted (" + responseCode + "): " Log.w(TAG, "Final variant not blacklisted (" + responseCode + "): "
+ chunk.dataSpec.uri); + chunk.dataSpec.uri);
enabledVariantBlacklistTimes[enabledVariantIndex] = 0; enabledVariantBlacklistFlags[enabledVariantIndex] = false;
return false; return false;
} }
} }
@ -644,57 +606,29 @@ public class HlsChunkSource {
private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) { private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) {
clearStaleBlacklistedVariants(); clearStaleBlacklistedVariants();
long bitrateEstimate = bandwidthMeter.getBitrateEstimate(); long switchingOverlapUs;
if (enabledVariantBlacklistTimes[selectedVariantIndex] != 0) { List<TsChunk> queue;
// The current variant has been blacklisted, so we have no choice but to re-evaluate. if (previousTsChunk != null) {
return getVariantIndexForBandwidth(bitrateEstimate); switchingOverlapUs = adaptiveMode == ADAPTIVE_MODE_SPLICE
? previousTsChunk.endTimeUs - previousTsChunk.startTimeUs : 0;
queue = Collections.singletonList(previousTsChunk);
} else {
switchingOverlapUs = 0;
queue = Collections.<TsChunk>emptyList();
} }
if (previousTsChunk == null) { if (enabledVariants.length > 1) {
// Don't consider switching if we don't have a previous chunk. adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, switchingOverlapUs,
return selectedVariantIndex; enabledVariantBlacklistFlags, evaluation);
} else {
evaluation.format = enabledVariants[0].format;
evaluation.trigger = Chunk.TRIGGER_MANUAL;
} }
if (bitrateEstimate == BandwidthMeter.NO_ESTIMATE) {
// Don't consider switching if we don't have a bandwidth estimate.
return selectedVariantIndex;
}
int idealIndex = getVariantIndexForBandwidth(bitrateEstimate);
if (idealIndex == selectedVariantIndex) {
// We're already using the ideal variant.
return selectedVariantIndex;
}
// We're not using the ideal variant for the available bandwidth, but only switch if the
// conditions are appropriate.
long bufferedPositionUs = adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs
: previousTsChunk.endTimeUs;
long bufferedUs = bufferedPositionUs - playbackPositionUs;
if (enabledVariantBlacklistTimes[selectedVariantIndex] != 0
|| (idealIndex > selectedVariantIndex && bufferedUs < maxBufferDurationToSwitchDownUs)
|| (idealIndex < selectedVariantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) {
// Switch variant.
return idealIndex;
}
// Stick with the current variant for now.
return selectedVariantIndex;
}
private int getVariantIndexForBandwidth(long bitrateEstimate) {
if (bitrateEstimate == BandwidthMeter.NO_ESTIMATE) {
// Select the lowest quality.
bitrateEstimate = 0;
}
int effectiveBitrate = (int) (bitrateEstimate * BANDWIDTH_FRACTION);
int lowestQualityEnabledVariantIndex = -1;
for (int i = 0; i < enabledVariants.length; i++) { for (int i = 0; i < enabledVariants.length; i++) {
if (enabledVariantBlacklistTimes[i] == 0) { if (enabledVariants[i].format == evaluation.format) {
if (enabledVariants[i].format.bitrate <= effectiveBitrate) {
return i; return i;
} }
lowestQualityEnabledVariantIndex = i;
} }
} throw new IllegalStateException();
// At least one variant should always be enabled.
Assertions.checkState(lowestQualityEnabledVariantIndex != -1);
return lowestQualityEnabledVariantIndex;
} }
private boolean shouldRerequestLiveMediaPlaylist(int nextVariantIndex) { private boolean shouldRerequestLiveMediaPlaylist(int nextVariantIndex) {
@ -760,8 +694,8 @@ public class HlsChunkSource {
} }
private boolean allVariantsBlacklisted() { private boolean allVariantsBlacklisted() {
for (int i = 0; i < enabledVariantBlacklistTimes.length; i++) { for (int i = 0; i < enabledVariantBlacklistFlags.length; i++) {
if (enabledVariantBlacklistTimes[i] == 0) { if (!enabledVariantBlacklistFlags[i]) {
return false; return false;
} }
} }
@ -770,10 +704,10 @@ public class HlsChunkSource {
private void clearStaleBlacklistedVariants() { private void clearStaleBlacklistedVariants() {
long currentTime = SystemClock.elapsedRealtime(); long currentTime = SystemClock.elapsedRealtime();
for (int i = 0; i < enabledVariantBlacklistTimes.length; i++) { for (int i = 0; i < enabledVariantBlacklistFlags.length; i++) {
if (enabledVariantBlacklistTimes[i] != 0 if (enabledVariantBlacklistFlags[i]
&& currentTime - enabledVariantBlacklistTimes[i] > DEFAULT_PLAYLIST_BLACKLIST_MS) { && currentTime - enabledVariantBlacklistTimes[i] > DEFAULT_PLAYLIST_BLACKLIST_MS) {
enabledVariantBlacklistTimes[i] = 0; enabledVariantBlacklistFlags[i] = false;
} }
} }
} }

View File

@ -81,6 +81,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
// Properties of enabled tracks. // Properties of enabled tracks.
private Format[] enabledFormats; private Format[] enabledFormats;
private boolean[] adaptiveFormatBlacklistFlags;
private IOException fatalError; private IOException fatalError;
@ -171,7 +172,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
Arrays.sort(enabledFormats, new DecreasingBandwidthComparator()); Arrays.sort(enabledFormats, new DecreasingBandwidthComparator());
if (enabledFormats.length > 1) { if (enabledFormats.length > 1) {
adaptiveFormatEvaluator.enable(); adaptiveFormatEvaluator.enable(enabledFormats);
adaptiveFormatBlacklistFlags = new boolean[tracks.length];
} }
} }
@ -221,7 +223,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
evaluation.queueSize = queue.size(); evaluation.queueSize = queue.size();
if (enabledFormats.length > 1) { if (enabledFormats.length > 1) {
adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, enabledFormats, evaluation); adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, 0, adaptiveFormatBlacklistFlags,
evaluation);
} else { } else {
evaluation.format = enabledFormats[0]; evaluation.format = enabledFormats[0];
evaluation.trigger = Chunk.TRIGGER_MANUAL; evaluation.trigger = Chunk.TRIGGER_MANUAL;
@ -316,7 +319,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
if (enabledFormats.length > 1) { if (enabledFormats.length > 1) {
adaptiveFormatEvaluator.disable(); adaptiveFormatEvaluator.disable();
} }
evaluation.format = null; evaluation.clear();
fatalError = null; fatalError = null;
} }

View File

@ -30,15 +30,21 @@ public final class DefaultBandwidthMeter implements BandwidthMeter {
public static final int DEFAULT_MAX_WEIGHT = 2000; public static final int DEFAULT_MAX_WEIGHT = 2000;
private static final int ELAPSED_MILLIS_FOR_ESTIMATE = 2000;
private static final int BYTES_TRANSFERRED_FOR_ESTIMATE = 512 * 1024;
private final Handler eventHandler; private final Handler eventHandler;
private final EventListener eventListener; private final EventListener eventListener;
private final Clock clock; private final Clock clock;
private final SlidingPercentile slidingPercentile; private final SlidingPercentile slidingPercentile;
private long bytesAccumulator;
private long startTimeMs;
private long bitrateEstimate;
private int streamCount; private int streamCount;
private long sampleStartTimeMs;
private long sampleBytesTransferred;
private long totalElapsedTimeMs;
private long totalBytesTransferred;
private long bitrateEstimate;
public DefaultBandwidthMeter() { public DefaultBandwidthMeter() {
this(null, null); this(null, null);
@ -73,34 +79,38 @@ public final class DefaultBandwidthMeter implements BandwidthMeter {
@Override @Override
public synchronized void onTransferStart() { public synchronized void onTransferStart() {
if (streamCount == 0) { if (streamCount == 0) {
startTimeMs = clock.elapsedRealtime(); sampleStartTimeMs = clock.elapsedRealtime();
} }
streamCount++; streamCount++;
} }
@Override @Override
public synchronized void onBytesTransferred(int bytes) { public synchronized void onBytesTransferred(int bytes) {
bytesAccumulator += bytes; sampleBytesTransferred += bytes;
} }
@Override @Override
public synchronized void onTransferEnd() { public synchronized void onTransferEnd() {
Assertions.checkState(streamCount > 0); Assertions.checkState(streamCount > 0);
long nowMs = clock.elapsedRealtime(); long nowMs = clock.elapsedRealtime();
int elapsedMs = (int) (nowMs - startTimeMs); int sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs);
if (elapsedMs > 0) { totalElapsedTimeMs += sampleElapsedTimeMs;
float bitsPerSecond = (bytesAccumulator * 8000) / elapsedMs; totalBytesTransferred += sampleBytesTransferred;
slidingPercentile.addSample((int) Math.sqrt(bytesAccumulator), bitsPerSecond); if (sampleElapsedTimeMs > 0) {
float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f); float bitsPerSecond = (sampleBytesTransferred * 8000) / sampleElapsedTimeMs;
bitrateEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE slidingPercentile.addSample((int) Math.sqrt(sampleBytesTransferred), bitsPerSecond);
: (long) bandwidthEstimateFloat; if (totalElapsedTimeMs >= ELAPSED_MILLIS_FOR_ESTIMATE
notifyBandwidthSample(elapsedMs, bytesAccumulator, bitrateEstimate); || totalBytesTransferred >= BYTES_TRANSFERRED_FOR_ESTIMATE) {
float bitrateEstimateFloat = slidingPercentile.getPercentile(0.5f);
bitrateEstimate = Float.isNaN(bitrateEstimateFloat) ? NO_ESTIMATE
: (long) bitrateEstimateFloat;
} }
streamCount--;
if (streamCount > 0) {
startTimeMs = nowMs;
} }
bytesAccumulator = 0; notifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, bitrateEstimate);
if (--streamCount > 0) {
sampleStartTimeMs = nowMs;
}
sampleBytesTransferred = 0;
} }
private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) { private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) {