Commit c48dd4f3 by olly Committed by Oliver Woodman

Use FormatEvaluator for HLS.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=114743795
parent a7adcce0
......@@ -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.
* <p>
* 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.
* <p>
* 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;
}
}
/**
* Always selects the first format.
* Clears {@link #format} and sets {@link #trigger} to {@link Chunk#TRIGGER_INITIAL}.
*/
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];
public void clear() {
format = null;
trigger = Chunk.TRIGGER_INITIAL;
}
}
/**
* 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 (!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];
}
}
......
......@@ -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;
......
......@@ -81,6 +81,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
// Properties of enabled tracks.
private Format[] enabledFormats;
private boolean[] adaptiveFormatBlacklistFlags;
private IOException fatalError;
......@@ -171,7 +172,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
Arrays.sort(enabledFormats, new DecreasingBandwidthComparator());
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();
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;
......@@ -316,7 +319,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
if (enabledFormats.length > 1) {
adaptiveFormatEvaluator.disable();
}
evaluation.format = null;
evaluation.clear();
fatalError = null;
}
......
......@@ -30,15 +30,21 @@ public final class DefaultBandwidthMeter implements BandwidthMeter {
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 EventListener eventListener;
private final Clock clock;
private final SlidingPercentile slidingPercentile;
private long bytesAccumulator;
private long startTimeMs;
private long bitrateEstimate;
private int streamCount;
private long sampleStartTimeMs;
private long sampleBytesTransferred;
private long totalElapsedTimeMs;
private long totalBytesTransferred;
private long bitrateEstimate;
public DefaultBandwidthMeter() {
this(null, null);
......@@ -73,34 +79,38 @@ public final class DefaultBandwidthMeter implements BandwidthMeter {
@Override
public synchronized void onTransferStart() {
if (streamCount == 0) {
startTimeMs = clock.elapsedRealtime();
sampleStartTimeMs = clock.elapsedRealtime();
}
streamCount++;
}
@Override
public synchronized void onBytesTransferred(int bytes) {
bytesAccumulator += bytes;
sampleBytesTransferred += bytes;
}
@Override
public synchronized void onTransferEnd() {
Assertions.checkState(streamCount > 0);
long nowMs = clock.elapsedRealtime();
int elapsedMs = (int) (nowMs - startTimeMs);
if (elapsedMs > 0) {
float bitsPerSecond = (bytesAccumulator * 8000) / elapsedMs;
slidingPercentile.addSample((int) Math.sqrt(bytesAccumulator), bitsPerSecond);
float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f);
bitrateEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE
: (long) bandwidthEstimateFloat;
notifyBandwidthSample(elapsedMs, bytesAccumulator, bitrateEstimate);
}
streamCount--;
if (streamCount > 0) {
startTimeMs = nowMs;
}
bytesAccumulator = 0;
int sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs);
totalElapsedTimeMs += sampleElapsedTimeMs;
totalBytesTransferred += sampleBytesTransferred;
if (sampleElapsedTimeMs > 0) {
float bitsPerSecond = (sampleBytesTransferred * 8000) / sampleElapsedTimeMs;
slidingPercentile.addSample((int) Math.sqrt(sampleBytesTransferred), bitsPerSecond);
if (totalElapsedTimeMs >= ELAPSED_MILLIS_FOR_ESTIMATE
|| totalBytesTransferred >= BYTES_TRANSFERRED_FOR_ESTIMATE) {
float bitrateEstimateFloat = slidingPercentile.getPercentile(0.5f);
bitrateEstimate = Float.isNaN(bitrateEstimateFloat) ? NO_ESTIMATE
: (long) bitrateEstimateFloat;
}
}
notifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, bitrateEstimate);
if (--streamCount > 0) {
sampleStartTimeMs = nowMs;
}
sampleBytesTransferred = 0;
}
private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment