Use FormatEvaluator for HLS.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=114743795
This commit is contained in:
parent
a7adcce018
commit
c48dd4f3e3
@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user