/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.exoplayer.chunk; import com.google.android.exoplayer.upstream.BandwidthMeter; import java.util.List; import java.util.Random; /** * Selects from a number of available formats during playback. */ public interface FormatEvaluator { /** * The trigger for the initial format selection. */ static final int TRIGGER_INITIAL = 0; /** * The trigger for a format selection that was triggered by the user. */ static final int TRIGGER_MANUAL = 1; /** * The trigger for an adaptive format selection. */ static final int TRIGGER_ADAPTIVE = 2; /** * Implementations may define custom trigger codes greater than or equal to this value. */ static final int TRIGGER_CUSTOM_BASE = 10000; /** * Enables the evaluator. */ void enable(); /** * Disables the evaluator. */ void disable(); /** * 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. * * @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. */ // TODO: Pass more useful information into this method, and finalize the interface. void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs, Format[] formats, Evaluation evaluation); /** * A format evaluation. */ public static final class Evaluation { /** * The desired size of the queue. */ public int queueSize; /** * The sticky reason for the format selection. */ public int trigger; /** * The selected format. */ public Format format; public Evaluation() { trigger = TRIGGER_INITIAL; } } /** * Always selects the first format. */ public static 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 class RandomEvaluator implements FormatEvaluator { private final Random random; public RandomEvaluator() { this.random = new Random(); } @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) { Format newFormat = formats[random.nextInt(formats.length)]; if (evaluation.format != null && !evaluation.format.id.equals(newFormat.id)) { evaluation.trigger = TRIGGER_ADAPTIVE; } evaluation.format = newFormat; } } /** * An adaptive evaluator for video formats, which attempts to select the best quality possible * given the current network conditions and state of the buffer. * <p> * This implementation should be used for video only, and should not be used for audio. It is a * reference implementation only. It is recommended that application developers implement their * own adaptive evaluator to more precisely suit their use case. */ public static class AdaptiveEvaluator implements FormatEvaluator { public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000; public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000; public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000; public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000; public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; private final BandwidthMeter bandwidthMeter; private final int maxInitialBitrate; private final long minDurationForQualityIncreaseUs; private final long maxDurationForQualityDecreaseUs; private final long minDurationToRetainAfterDiscardUs; private final float bandwidthFraction; /** * @param bandwidthMeter Provides an estimate of the currently available bandwidth. */ public AdaptiveEvaluator(BandwidthMeter bandwidthMeter) { this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION); } /** * @param bandwidthMeter Provides an estimate of the currently available bandwidth. * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed * when bandwidthMeter cannot provide an estimate due to playback having only just started. * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for * the evaluator to consider switching to a higher quality format. * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for * the evaluator to consider switching to a lower quality format. * @param minDurationToRetainAfterDiscardMs When switching to a significantly higher quality * format, the evaluator may discard some of the media that it has already buffered at the * lower quality, so as to switch up to the higher quality faster. This is the minimum * duration of media that must be retained at the lower quality. * @param bandwidthFraction The fraction of the available bandwidth that the evaluator should * consider available for use. Setting to a value less than 1 is recommended to account * for inaccuracies in the bandwidth estimator. */ public AdaptiveEvaluator(BandwidthMeter bandwidthMeter, int maxInitialBitrate, int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, int minDurationToRetainAfterDiscardMs, float bandwidthFraction) { this.bandwidthMeter = bandwidthMeter; this.maxInitialBitrate = maxInitialBitrate; this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L; this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L; this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L; this.bandwidthFraction = bandwidthFraction; } @Override public void enable() { // Do nothing. } @Override public void disable() { // Do nothing. } @Override public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs, Format[] formats, Evaluation evaluation) { long bufferedDurationUs = queue.isEmpty() ? 0 : queue.get(queue.size() - 1).endTimeUs - playbackPositionUs; Format current = evaluation.format; Format ideal = determineIdealFormat(formats, bandwidthMeter.getBitrateEstimate()); boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate; boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate; if (isHigher) { if (bufferedDurationUs < minDurationForQualityIncreaseUs) { // The ideal format is a higher quality, but we have insufficient buffer to // safely switch up. Defer switching up for now. ideal = current; } else if (bufferedDurationUs >= minDurationToRetainAfterDiscardUs) { // We're switching from an SD stream to a stream of higher resolution. Consider // discarding already buffered media chunks. Specifically, discard media chunks starting // from the first one that is of lower bandwidth, lower resolution and that is not HD. for (int i = 1; i < queue.size(); i++) { MediaChunk thisChunk = queue.get(i); long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs; if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs && thisChunk.format.bitrate < ideal.bitrate && thisChunk.format.height < ideal.height && thisChunk.format.height < 720 && thisChunk.format.width < 1280) { // Discard chunks from this one onwards. evaluation.queueSize = i; break; } } } } else if (isLower && current != null && bufferedDurationUs >= maxDurationForQualityDecreaseUs) { // The ideal format is a lower quality, but we have sufficient buffer to defer switching // down for now. ideal = current; } if (current != null && ideal != current) { evaluation.trigger = FormatEvaluator.TRIGGER_ADAPTIVE; } evaluation.format = ideal; } /** * Compute the ideal format ignoring buffer health. */ protected Format determineIdealFormat(Format[] formats, long bitrateEstimate) { long effectiveBitrate = computeEffectiveBitrateEstimate(bitrateEstimate); for (int i = 0; i < formats.length; i++) { Format format = formats[i]; if (format.bitrate <= effectiveBitrate) { return format; } } // We didn't manage to calculate a suitable format. Return the lowest quality format. return formats[formats.length - 1]; } /** * Apply overhead factor, or default value in absence of estimate. */ protected long computeEffectiveBitrateEstimate(long bitrateEstimate) { return bitrateEstimate == BandwidthMeter.NO_ESTIMATE ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction); } } }