diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java | 761 |
1 files changed, 761 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java new file mode 100644 index 0000000000..33f8606e9b --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -0,0 +1,761 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SimpleExoPlayer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** + * A bandwidth based adaptive {@link TrackSelection}, whose selected track is updated to be the one + * of highest quality given the current network conditions and the state of the buffer. + */ +public class AdaptiveTrackSelection extends BaseTrackSelection { + + /** Factory for {@link AdaptiveTrackSelection} instances. */ + public static class Factory implements TrackSelection.Factory { + + @Nullable private final BandwidthMeter bandwidthMeter; + private final int minDurationForQualityIncreaseMs; + private final int maxDurationForQualityDecreaseMs; + private final int minDurationToRetainAfterDiscardMs; + private final float bandwidthFraction; + private final float bufferedFractionToLiveEdgeForQualityIncrease; + private final long minTimeBetweenBufferReevaluationMs; + private final Clock clock; + + /** Creates an adaptive track selection factory with default parameters. */ + public Factory() { + this( + 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, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); + } + + /** + * @deprecated Use {@link #Factory()} instead. Custom bandwidth meter should be directly passed + * to the player in {@link SimpleExoPlayer.Builder}. + */ + @Deprecated + @SuppressWarnings("deprecation") + public Factory(BandwidthMeter bandwidthMeter) { + this( + bandwidthMeter, + 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, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); + } + + /** + * Creates an adaptive track selection factory. + * + * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the + * selected track to switch to one of higher quality. + * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the + * selected track to switch to one of lower quality. + * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher + * quality, the selection may indicate that media already buffered at the lower quality can + * be discarded to speed up the switch. 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 selection should + * consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. + */ + public Factory( + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction) { + this( + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bandwidthFraction, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); + } + + /** + * @deprecated Use {@link #Factory(int, int, int, float)} instead. Custom bandwidth meter should + * be directly passed to the player in {@link SimpleExoPlayer.Builder}. + */ + @Deprecated + @SuppressWarnings("deprecation") + public Factory( + BandwidthMeter bandwidthMeter, + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction) { + this( + bandwidthMeter, + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bandwidthFraction, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); + } + + /** + * Creates an adaptive track selection factory. + * + * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the + * selected track to switch to one of higher quality. + * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the + * selected track to switch to one of lower quality. + * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher + * quality, the selection may indicate that media already buffered at the lower quality can + * be discarded to speed up the switch. 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 selection should + * consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the + * duration from current playback position to the live edge that has to be buffered before + * the selected track can be switched to one of higher quality. This parameter is only + * applied when the playback position is closer to the live edge than {@code + * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher + * quality from happening. + * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its + * buffer and discard some chunks of lower quality to improve the playback quality if + * network conditions have changed. This is the minimum duration between 2 consecutive + * buffer reevaluation calls. + * @param clock A {@link Clock}. + */ + @SuppressWarnings("deprecation") + public Factory( + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { + this( + /* bandwidthMeter= */ null, + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bandwidthFraction, + bufferedFractionToLiveEdgeForQualityIncrease, + minTimeBetweenBufferReevaluationMs, + clock); + } + + /** + * @deprecated Use {@link #Factory(int, int, int, float, float, long, Clock)} instead. Custom + * bandwidth meter should be directly passed to the player in {@link + * SimpleExoPlayer.Builder}. + */ + @Deprecated + public Factory( + @Nullable BandwidthMeter bandwidthMeter, + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { + this.bandwidthMeter = bandwidthMeter; + this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs; + this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs; + this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs; + this.bandwidthFraction = bandwidthFraction; + this.bufferedFractionToLiveEdgeForQualityIncrease = + bufferedFractionToLiveEdgeForQualityIncrease; + this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; + this.clock = clock; + } + + @Override + public final @NullableType TrackSelection[] createTrackSelections( + @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { + if (this.bandwidthMeter != null) { + bandwidthMeter = this.bandwidthMeter; + } + TrackSelection[] selections = new TrackSelection[definitions.length]; + int totalFixedBandwidth = 0; + for (int i = 0; i < definitions.length; i++) { + Definition definition = definitions[i]; + if (definition != null && definition.tracks.length == 1) { + // Make fixed selections first to know their total bandwidth. + selections[i] = + new FixedTrackSelection( + definition.group, definition.tracks[0], definition.reason, definition.data); + int trackBitrate = definition.group.getFormat(definition.tracks[0]).bitrate; + if (trackBitrate != Format.NO_VALUE) { + totalFixedBandwidth += trackBitrate; + } + } + } + List<AdaptiveTrackSelection> adaptiveSelections = new ArrayList<>(); + for (int i = 0; i < definitions.length; i++) { + Definition definition = definitions[i]; + if (definition != null && definition.tracks.length > 1) { + AdaptiveTrackSelection adaptiveSelection = + createAdaptiveTrackSelection( + definition.group, bandwidthMeter, definition.tracks, totalFixedBandwidth); + adaptiveSelections.add(adaptiveSelection); + selections[i] = adaptiveSelection; + } + } + if (adaptiveSelections.size() > 1) { + long[][] adaptiveTrackBitrates = new long[adaptiveSelections.size()][]; + for (int i = 0; i < adaptiveSelections.size(); i++) { + AdaptiveTrackSelection adaptiveSelection = adaptiveSelections.get(i); + adaptiveTrackBitrates[i] = new long[adaptiveSelection.length()]; + for (int j = 0; j < adaptiveSelection.length(); j++) { + adaptiveTrackBitrates[i][j] = + adaptiveSelection.getFormat(adaptiveSelection.length() - j - 1).bitrate; + } + } + long[][][] bandwidthCheckpoints = getAllocationCheckpoints(adaptiveTrackBitrates); + for (int i = 0; i < adaptiveSelections.size(); i++) { + adaptiveSelections + .get(i) + .experimental_setBandwidthAllocationCheckpoints(bandwidthCheckpoints[i]); + } + } + return selections; + } + + /** + * Creates a single adaptive selection for the given group, bandwidth meter and tracks. + * + * @param group The {@link TrackGroup}. + * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. + * @param tracks The indices of the selected tracks in the track group. + * @param totalFixedTrackBandwidth The total bandwidth used by all non-adaptive tracks, in bits + * per second. + * @return An {@link AdaptiveTrackSelection} for the specified tracks. + */ + protected AdaptiveTrackSelection createAdaptiveTrackSelection( + TrackGroup group, + BandwidthMeter bandwidthMeter, + int[] tracks, + int totalFixedTrackBandwidth) { + return new AdaptiveTrackSelection( + group, + tracks, + new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, totalFixedTrackBandwidth), + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bufferedFractionToLiveEdgeForQualityIncrease, + minTimeBetweenBufferReevaluationMs, + clock); + } + } + + 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.7f; + public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f; + public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000; + + private final BandwidthProvider bandwidthProvider; + private final long minDurationForQualityIncreaseUs; + private final long maxDurationForQualityDecreaseUs; + private final long minDurationToRetainAfterDiscardUs; + private final float bufferedFractionToLiveEdgeForQualityIncrease; + private final long minTimeBetweenBufferReevaluationMs; + private final Clock clock; + + private float playbackSpeed; + private int selectedIndex; + private int reason; + private long lastBufferEvaluationMs; + + /** + * @param group The {@link TrackGroup}. + * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be + * empty. May be in any order. + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + */ + public AdaptiveTrackSelection(TrackGroup group, int[] tracks, + BandwidthMeter bandwidthMeter) { + this( + group, + tracks, + bandwidthMeter, + /* reservedBandwidth= */ 0, + 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, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); + } + + /** + * @param group The {@link TrackGroup}. + * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be + * empty. May be in any order. + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param reservedBandwidth The reserved bandwidth, which shouldn't be considered available for + * use, in bits per second. + * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the + * selected track to switch to one of higher quality. + * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the + * selected track to switch to one of lower quality. + * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher + * quality, the selection may indicate that media already buffered at the lower quality can be + * discarded to speed up the switch. 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 selection should + * consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the + * duration from current playback position to the live edge that has to be buffered before the + * selected track can be switched to one of higher quality. This parameter is only applied + * when the playback position is closer to the live edge than {@code + * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher + * quality from happening. + * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its + * buffer and discard some chunks of lower quality to improve the playback quality if network + * condition has changed. This is the minimum duration between 2 consecutive buffer + * reevaluation calls. + */ + public AdaptiveTrackSelection( + TrackGroup group, + int[] tracks, + BandwidthMeter bandwidthMeter, + long reservedBandwidth, + long minDurationForQualityIncreaseMs, + long maxDurationForQualityDecreaseMs, + long minDurationToRetainAfterDiscardMs, + float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { + this( + group, + tracks, + new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, reservedBandwidth), + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bufferedFractionToLiveEdgeForQualityIncrease, + minTimeBetweenBufferReevaluationMs, + clock); + } + + private AdaptiveTrackSelection( + TrackGroup group, + int[] tracks, + BandwidthProvider bandwidthProvider, + long minDurationForQualityIncreaseMs, + long maxDurationForQualityDecreaseMs, + long minDurationToRetainAfterDiscardMs, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { + super(group, tracks); + this.bandwidthProvider = bandwidthProvider; + this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L; + this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L; + this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L; + this.bufferedFractionToLiveEdgeForQualityIncrease = + bufferedFractionToLiveEdgeForQualityIncrease; + this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; + this.clock = clock; + playbackSpeed = 1f; + reason = C.SELECTION_REASON_UNKNOWN; + lastBufferEvaluationMs = C.TIME_UNSET; + } + + /** + * Sets checkpoints to determine the allocation bandwidth based on the total bandwidth. + * + * @param allocationCheckpoints List of checkpoints. Each element must be a long[2], with [0] + * being the total bandwidth and [1] being the allocated bandwidth. + */ + public void experimental_setBandwidthAllocationCheckpoints(long[][] allocationCheckpoints) { + ((DefaultBandwidthProvider) bandwidthProvider) + .experimental_setBandwidthAllocationCheckpoints(allocationCheckpoints); + } + + @Override + public void enable() { + lastBufferEvaluationMs = C.TIME_UNSET; + } + + @Override + public void onPlaybackSpeed(float playbackSpeed) { + this.playbackSpeed = playbackSpeed; + } + + @Override + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List<? extends MediaChunk> queue, + MediaChunkIterator[] mediaChunkIterators) { + long nowMs = clock.elapsedRealtime(); + + // Make initial selection + if (reason == C.SELECTION_REASON_UNKNOWN) { + reason = C.SELECTION_REASON_INITIAL; + selectedIndex = determineIdealSelectedIndex(nowMs); + return; + } + + // Stash the current selection, then make a new one. + int currentSelectedIndex = selectedIndex; + selectedIndex = determineIdealSelectedIndex(nowMs); + if (selectedIndex == currentSelectedIndex) { + return; + } + + if (!isBlacklisted(currentSelectedIndex, nowMs)) { + // Revert back to the current selection if conditions are not suitable for switching. + Format currentFormat = getFormat(currentSelectedIndex); + Format selectedFormat = getFormat(selectedIndex); + if (selectedFormat.bitrate > currentFormat.bitrate + && bufferedDurationUs < minDurationForQualityIncreaseUs(availableDurationUs)) { + // The selected track is a higher quality, but we have insufficient buffer to safely switch + // up. Defer switching up for now. + selectedIndex = currentSelectedIndex; + } else if (selectedFormat.bitrate < currentFormat.bitrate + && bufferedDurationUs >= maxDurationForQualityDecreaseUs) { + // The selected track is a lower quality, but we have sufficient buffer to defer switching + // down for now. + selectedIndex = currentSelectedIndex; + } + } + // If we adapted, update the trigger. + if (selectedIndex != currentSelectedIndex) { + reason = C.SELECTION_REASON_ADAPTIVE; + } + } + + @Override + public int getSelectedIndex() { + return selectedIndex; + } + + @Override + public int getSelectionReason() { + return reason; + } + + @Override + @Nullable + public Object getSelectionData() { + return null; + } + + @Override + public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) { + long nowMs = clock.elapsedRealtime(); + if (!shouldEvaluateQueueSize(nowMs)) { + return queue.size(); + } + + lastBufferEvaluationMs = nowMs; + if (queue.isEmpty()) { + return 0; + } + + int queueSize = queue.size(); + MediaChunk lastChunk = queue.get(queueSize - 1); + long playoutBufferedDurationBeforeLastChunkUs = + Util.getPlayoutDurationForMediaDuration( + lastChunk.startTimeUs - playbackPositionUs, playbackSpeed); + long minDurationToRetainAfterDiscardUs = getMinDurationToRetainAfterDiscardUs(); + if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) { + return queueSize; + } + int idealSelectedIndex = determineIdealSelectedIndex(nowMs); + Format idealFormat = getFormat(idealSelectedIndex); + // If the chunks contain video, discard from the first SD chunk beyond + // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal + // track. + for (int i = 0; i < queueSize; i++) { + MediaChunk chunk = queue.get(i); + Format format = chunk.trackFormat; + long mediaDurationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs; + long playoutDurationBeforeThisChunkUs = + Util.getPlayoutDurationForMediaDuration(mediaDurationBeforeThisChunkUs, playbackSpeed); + if (playoutDurationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs + && format.bitrate < idealFormat.bitrate + && format.height != Format.NO_VALUE && format.height < 720 + && format.width != Format.NO_VALUE && format.width < 1280 + && format.height < idealFormat.height) { + return i; + } + } + return queueSize; + } + + /** + * Called when updating the selected track to determine whether a candidate track can be selected. + * + * @param format The {@link Format} of the candidate track. + * @param trackBitrate The estimated bitrate of the track. May differ from {@link Format#bitrate} + * if a more accurate estimate of the current track bitrate is available. + * @param playbackSpeed The current playback speed. + * @param effectiveBitrate The bitrate available to this selection. + * @return Whether this {@link Format} can be selected. + */ + @SuppressWarnings("unused") + protected boolean canSelectFormat( + Format format, int trackBitrate, float playbackSpeed, long effectiveBitrate) { + return Math.round(trackBitrate * playbackSpeed) <= effectiveBitrate; + } + + /** + * Called from {@link #evaluateQueueSize(long, List)} to determine whether an evaluation should be + * performed. + * + * @param nowMs The current value of {@link Clock#elapsedRealtime()}. + * @return Whether an evaluation should be performed. + */ + protected boolean shouldEvaluateQueueSize(long nowMs) { + return lastBufferEvaluationMs == C.TIME_UNSET + || nowMs - lastBufferEvaluationMs >= minTimeBetweenBufferReevaluationMs; + } + + /** + * Called from {@link #evaluateQueueSize(long, List)} to determine the minimum duration of buffer + * to retain after discarding chunks. + * + * @return The minimum duration of buffer to retain after discarding chunks, in microseconds. + */ + protected long getMinDurationToRetainAfterDiscardUs() { + return minDurationToRetainAfterDiscardUs; + } + + /** + * Computes the ideal selected index ignoring buffer health. + * + * @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link + * Long#MIN_VALUE} to ignore blacklisting. + */ + private int determineIdealSelectedIndex(long nowMs) { + long effectiveBitrate = bandwidthProvider.getAllocatedBandwidth(); + int lowestBitrateNonBlacklistedIndex = 0; + for (int i = 0; i < length; i++) { + if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { + Format format = getFormat(i); + if (canSelectFormat(format, format.bitrate, playbackSpeed, effectiveBitrate)) { + return i; + } else { + lowestBitrateNonBlacklistedIndex = i; + } + } + } + return lowestBitrateNonBlacklistedIndex; + } + + private long minDurationForQualityIncreaseUs(long availableDurationUs) { + boolean isAvailableDurationTooShort = availableDurationUs != C.TIME_UNSET + && availableDurationUs <= minDurationForQualityIncreaseUs; + return isAvailableDurationTooShort + ? (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease) + : minDurationForQualityIncreaseUs; + } + + /** Provides the allocated bandwidth. */ + private interface BandwidthProvider { + + /** Returns the allocated bitrate. */ + long getAllocatedBandwidth(); + } + + private static final class DefaultBandwidthProvider implements BandwidthProvider { + + private final BandwidthMeter bandwidthMeter; + private final float bandwidthFraction; + private final long reservedBandwidth; + + @Nullable private long[][] allocationCheckpoints; + + /* package */ + // the constructor does not initialize fields: allocationCheckpoints + @SuppressWarnings("nullness:initialization.fields.uninitialized") + DefaultBandwidthProvider( + BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) { + this.bandwidthMeter = bandwidthMeter; + this.bandwidthFraction = bandwidthFraction; + this.reservedBandwidth = reservedBandwidth; + } + + // unboxing a possibly-null reference allocationCheckpoints[nextIndex][0] + @SuppressWarnings("nullness:unboxing.of.nullable") + @Override + public long getAllocatedBandwidth() { + long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction); + long allocatableBandwidth = Math.max(0L, totalBandwidth - reservedBandwidth); + if (allocationCheckpoints == null) { + return allocatableBandwidth; + } + int nextIndex = 1; + while (nextIndex < allocationCheckpoints.length - 1 + && allocationCheckpoints[nextIndex][0] < allocatableBandwidth) { + nextIndex++; + } + long[] previous = allocationCheckpoints[nextIndex - 1]; + long[] next = allocationCheckpoints[nextIndex]; + float fractionBetweenCheckpoints = + (float) (allocatableBandwidth - previous[0]) / (next[0] - previous[0]); + return previous[1] + (long) (fractionBetweenCheckpoints * (next[1] - previous[1])); + } + + /* package */ void experimental_setBandwidthAllocationCheckpoints( + long[][] allocationCheckpoints) { + Assertions.checkArgument(allocationCheckpoints.length >= 2); + this.allocationCheckpoints = allocationCheckpoints; + } + } + + /** + * Returns allocation checkpoints for allocating bandwidth between multiple adaptive track + * selections. + * + * @param trackBitrates Array of [selectionIndex][trackIndex] -> trackBitrate. + * @return Array of allocation checkpoints [selectionIndex][checkpointIndex][2] with [0]=total + * bandwidth at checkpoint and [1]=allocated bandwidth at checkpoint. + */ + private static long[][][] getAllocationCheckpoints(long[][] trackBitrates) { + // Algorithm: + // 1. Use log bitrates to treat all resolution update steps equally. + // 2. Distribute switch points for each selection equally in the same [0.0-1.0] range. + // 3. Switch up one format at a time in the order of the switch points. + double[][] logBitrates = getLogArrayValues(trackBitrates); + double[][] switchPoints = getSwitchPoints(logBitrates); + + // There will be (count(switch point) + 3) checkpoints: + // [0] = all zero, [1] = minimum bitrates, [2-(end-1)] = up-switch points, + // [end] = extra point to set slope for additional bitrate. + int checkpointCount = countArrayElements(switchPoints) + 3; + long[][][] checkpoints = new long[logBitrates.length][checkpointCount][2]; + int[] currentSelection = new int[logBitrates.length]; + setCheckpointValues(checkpoints, /* checkpointIndex= */ 1, trackBitrates, currentSelection); + for (int checkpointIndex = 2; checkpointIndex < checkpointCount - 1; checkpointIndex++) { + int nextUpdateIndex = 0; + double nextUpdateSwitchPoint = Double.MAX_VALUE; + for (int i = 0; i < logBitrates.length; i++) { + if (currentSelection[i] + 1 == logBitrates[i].length) { + continue; + } + double switchPoint = switchPoints[i][currentSelection[i]]; + if (switchPoint < nextUpdateSwitchPoint) { + nextUpdateSwitchPoint = switchPoint; + nextUpdateIndex = i; + } + } + currentSelection[nextUpdateIndex]++; + setCheckpointValues(checkpoints, checkpointIndex, trackBitrates, currentSelection); + } + for (long[][] points : checkpoints) { + points[checkpointCount - 1][0] = 2 * points[checkpointCount - 2][0]; + points[checkpointCount - 1][1] = 2 * points[checkpointCount - 2][1]; + } + return checkpoints; + } + + /** Converts all input values to Math.log(value). */ + private static double[][] getLogArrayValues(long[][] values) { + double[][] logValues = new double[values.length][]; + for (int i = 0; i < values.length; i++) { + logValues[i] = new double[values[i].length]; + for (int j = 0; j < values[i].length; j++) { + logValues[i][j] = values[i][j] == Format.NO_VALUE ? 0 : Math.log(values[i][j]); + } + } + return logValues; + } + + /** + * Returns idealized switch points for each switch between consecutive track selection bitrates. + * + * @param logBitrates Log bitrates with [selectionCount][formatCount]. + * @return Linearly distributed switch points in the range of [0.0-1.0]. + */ + private static double[][] getSwitchPoints(double[][] logBitrates) { + double[][] switchPoints = new double[logBitrates.length][]; + for (int i = 0; i < logBitrates.length; i++) { + switchPoints[i] = new double[logBitrates[i].length - 1]; + if (switchPoints[i].length == 0) { + continue; + } + double totalBitrateDiff = logBitrates[i][logBitrates[i].length - 1] - logBitrates[i][0]; + for (int j = 0; j < logBitrates[i].length - 1; j++) { + double switchBitrate = 0.5 * (logBitrates[i][j] + logBitrates[i][j + 1]); + switchPoints[i][j] = + totalBitrateDiff == 0.0 ? 1.0 : (switchBitrate - logBitrates[i][0]) / totalBitrateDiff; + } + } + return switchPoints; + } + + /** Returns total number of elements in a 2D array. */ + private static int countArrayElements(double[][] array) { + int count = 0; + for (double[] subArray : array) { + count += subArray.length; + } + return count; + } + + /** + * Sets checkpoint bitrates. + * + * @param checkpoints Output checkpoints with [selectionIndex][checkpointIndex][2] where [0]=Total + * bitrate and [1]=Allocated bitrate. + * @param checkpointIndex The checkpoint index. + * @param trackBitrates The track bitrates with [selectionIndex][trackIndex]. + * @param selectedTracks The indices of selected tracks for each selection for this checkpoint. + */ + private static void setCheckpointValues( + long[][][] checkpoints, int checkpointIndex, long[][] trackBitrates, int[] selectedTracks) { + long totalBitrate = 0; + for (int i = 0; i < checkpoints.length; i++) { + checkpoints[i][checkpointIndex][1] = trackBitrates[i][selectedTracks[i]]; + totalBitrate += checkpoints[i][checkpointIndex][1]; + } + for (long[][] points : checkpoints) { + points[checkpointIndex][0] = totalBitrate; + } + } +} |