Commit 4665390f by christosts Committed by Rohit Singh

Publish experimental bandwidth meter classes

PiperOrigin-RevId: 524846153
parent 2593c5f6
Showing with 2224 additions and 0 deletions
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import android.os.Handler;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
/** The interface for different bandwidth estimation strategies. */
public interface BandwidthEstimator {
long ESTIMATE_NOT_AVAILABLE = Long.MIN_VALUE;
/**
* Adds an {@link BandwidthMeter.EventListener}.
*
* @param eventHandler A handler for events.
* @param eventListener A listener of events.
*/
void addEventListener(Handler eventHandler, BandwidthMeter.EventListener eventListener);
/**
* Removes an {@link BandwidthMeter.EventListener}.
*
* @param eventListener The listener to be removed.
*/
void removeEventListener(BandwidthMeter.EventListener eventListener);
/**
* Called when a transfer is being initialized.
*
* @param source The {@link DataSource} performing the transfer.
*/
void onTransferInitializing(DataSource source);
/**
* Called when a transfer starts.
*
* @param source The {@link DataSource} performing the transfer.
*/
void onTransferStart(DataSource source);
/**
* Called incrementally during a transfer.
*
* @param source The {@link DataSource} performing the transfer.
* @param bytesTransferred The number of bytes transferred since the previous call to this method
*/
void onBytesTransferred(DataSource source, int bytesTransferred);
/**
* Called when a transfer ends.
*
* @param source The {@link DataSource} performing the transfer.
*/
void onTransferEnd(DataSource source);
/**
* Returns the bandwidth estimate in bits per second, or {@link #ESTIMATE_NOT_AVAILABLE} if there
* is no estimate available yet.
*/
long getBandwidthEstimate();
/**
* Notifies this estimator that a network change has been detected.
*
* @param newBandwidthEstimate The new initial bandwidth estimate based on network type.
*/
void onNetworkTypeChange(long newBandwidthEstimate);
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
/** The interface for different bandwidth estimation statistics. */
public interface BandwidthStatistic {
/**
* Adds a transfer sample to the statistic.
*
* @param bytes The number of bytes transferred.
* @param durationUs The duration of the transfer, in microseconds.
*/
void addSample(long bytes, long durationUs);
/**
* Returns the bandwidth estimate in bits per second, or {@link
* BandwidthEstimator#ESTIMATE_NOT_AVAILABLE} if there is no estimate available yet.
*/
long getBandwidthEstimate();
/**
* Resets the statistic. The statistic should drop all samples and reset to its initial state,
* similar to right after construction.
*/
void reset();
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.os.Handler;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Clock;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/**
* A {@link BandwidthEstimator} that captures a transfer sample each time all parallel transfers
* end.
*/
public class CombinedParallelSampleBandwidthEstimator implements BandwidthEstimator {
/** A builder to create {@link CombinedParallelSampleBandwidthEstimator} instances. */
public static class Builder {
private BandwidthStatistic bandwidthStatistic;
private int minSamples;
private long minBytesTransferred;
private Clock clock;
/** Creates a new builder instance. */
public Builder() {
bandwidthStatistic = new SlidingWeightedAverageBandwidthStatistic();
clock = Clock.DEFAULT;
}
/**
* Sets the {@link BandwidthStatistic} to be used by the estimator. By default, this is set to a
* {@link SlidingWeightedAverageBandwidthStatistic}.
*
* @param bandwidthStatistic The {@link BandwidthStatistic}.
* @return This builder for convenience.
*/
@CanIgnoreReturnValue
public Builder setBandwidthStatistic(BandwidthStatistic bandwidthStatistic) {
checkNotNull(bandwidthStatistic);
this.bandwidthStatistic = bandwidthStatistic;
return this;
}
/**
* Sets a minimum threshold of samples that need to be taken before the estimator can return a
* bandwidth estimate. By default, this is set to {@code 0}.
*
* @param minSamples The minimum number of samples.
* @return This builder for convenience.
*/
@CanIgnoreReturnValue
public Builder setMinSamples(int minSamples) {
checkArgument(minSamples >= 0);
this.minSamples = minSamples;
return this;
}
/**
* Sets a minimum threshold of bytes that need to be transferred before the estimator can return
* a bandwidth estimate. By default, this is set to {@code 0}.
*
* @param minBytesTransferred The minimum number of transferred bytes.
* @return This builder for convenience.
*/
@CanIgnoreReturnValue
public Builder setMinBytesTransferred(long minBytesTransferred) {
checkArgument(minBytesTransferred >= 0);
this.minBytesTransferred = minBytesTransferred;
return this;
}
/**
* Sets the {@link Clock} used by the estimator. By default, this is set to {@link
* Clock#DEFAULT}.
*
* @param clock The {@link Clock} to be used.
* @return This builder for convenience.
*/
@CanIgnoreReturnValue
@VisibleForTesting
/* package */ Builder setClock(Clock clock) {
this.clock = clock;
return this;
}
public CombinedParallelSampleBandwidthEstimator build() {
return new CombinedParallelSampleBandwidthEstimator(this);
}
}
private final BandwidthStatistic bandwidthStatistic;
private final int minSamples;
private final long minBytesTransferred;
private final BandwidthMeter.EventListener.EventDispatcher eventDispatcher;
private final Clock clock;
private int streamCount;
private long sampleStartTimeMs;
private long sampleBytesTransferred;
private long bandwidthEstimate;
private long lastReportedBandwidthEstimate;
private int totalSamplesAdded;
private long totalBytesTransferred;
private CombinedParallelSampleBandwidthEstimator(Builder builder) {
this.bandwidthStatistic = builder.bandwidthStatistic;
this.minSamples = builder.minSamples;
this.minBytesTransferred = builder.minBytesTransferred;
this.clock = builder.clock;
eventDispatcher = new BandwidthMeter.EventListener.EventDispatcher();
bandwidthEstimate = ESTIMATE_NOT_AVAILABLE;
lastReportedBandwidthEstimate = ESTIMATE_NOT_AVAILABLE;
}
@Override
public void addEventListener(Handler eventHandler, BandwidthMeter.EventListener eventListener) {
eventDispatcher.addListener(eventHandler, eventListener);
}
@Override
public void removeEventListener(BandwidthMeter.EventListener eventListener) {
eventDispatcher.removeListener(eventListener);
}
@Override
public void onTransferInitializing(DataSource source) {}
@Override
public void onTransferStart(DataSource source) {
if (streamCount == 0) {
sampleStartTimeMs = clock.elapsedRealtime();
}
streamCount++;
}
@Override
public void onBytesTransferred(DataSource source, int bytesTransferred) {
sampleBytesTransferred += bytesTransferred;
totalBytesTransferred += bytesTransferred;
}
@Override
public void onTransferEnd(DataSource source) {
checkState(streamCount > 0);
streamCount--;
if (streamCount > 0) {
return;
}
long nowMs = clock.elapsedRealtime();
long sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs);
if (sampleElapsedTimeMs > 0) {
bandwidthStatistic.addSample(sampleBytesTransferred, sampleElapsedTimeMs * 1000);
totalSamplesAdded++;
if (totalSamplesAdded > minSamples && totalBytesTransferred > minBytesTransferred) {
bandwidthEstimate = bandwidthStatistic.getBandwidthEstimate();
}
maybeNotifyBandwidthSample(
(int) sampleElapsedTimeMs, sampleBytesTransferred, bandwidthEstimate);
sampleBytesTransferred = 0;
}
}
@Override
public long getBandwidthEstimate() {
return bandwidthEstimate;
}
@Override
public void onNetworkTypeChange(long newBandwidthEstimate) {
long nowMs = clock.elapsedRealtime();
int sampleElapsedTimeMs = streamCount > 0 ? (int) (nowMs - sampleStartTimeMs) : 0;
maybeNotifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, newBandwidthEstimate);
bandwidthStatistic.reset();
bandwidthEstimate = ESTIMATE_NOT_AVAILABLE;
sampleStartTimeMs = nowMs;
sampleBytesTransferred = 0;
totalSamplesAdded = 0;
totalBytesTransferred = 0;
}
private void maybeNotifyBandwidthSample(
int elapsedMs, long bytesTransferred, long bandwidthEstimate) {
if ((bandwidthEstimate == ESTIMATE_NOT_AVAILABLE)
|| (elapsedMs == 0
&& bytesTransferred == 0
&& bandwidthEstimate == lastReportedBandwidthEstimate)) {
return;
}
lastReportedBandwidthEstimate = bandwidthEstimate;
eventDispatcher.bandwidthSample(elapsedMs, bytesTransferred, bandwidthEstimate);
}
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.android.exoplayer2.upstream.experimental.BandwidthEstimator.ESTIMATE_NOT_AVAILABLE;
/** A {@link BandwidthStatistic} that calculates estimates using an exponential weighted average. */
public class ExponentialWeightedAverageStatistic implements BandwidthStatistic {
/** The default smoothing factor. */
public static final double DEFAULT_SMOOTHING_FACTOR = 0.9999;
private final double smoothingFactor;
private long bitrateEstimate;
/** Creates an instance with {@link #DEFAULT_SMOOTHING_FACTOR}. */
public ExponentialWeightedAverageStatistic() {
this(DEFAULT_SMOOTHING_FACTOR);
}
/**
* Creates an instance.
*
* @param smoothingFactor The exponential smoothing factor.
*/
public ExponentialWeightedAverageStatistic(double smoothingFactor) {
this.smoothingFactor = smoothingFactor;
bitrateEstimate = ESTIMATE_NOT_AVAILABLE;
}
@Override
public void addSample(long bytes, long durationUs) {
long bitrate = bytes * 8_000_000 / durationUs;
if (bitrateEstimate == ESTIMATE_NOT_AVAILABLE) {
bitrateEstimate = bitrate;
return;
}
// Weight smoothing factor by sqrt(bytes).
double factor = Math.pow(smoothingFactor, Math.sqrt((double) bytes));
bitrateEstimate = (long) (factor * bitrateEstimate + (1f - factor) * bitrate);
}
@Override
public long getBandwidthEstimate() {
return bitrateEstimate;
}
@Override
public void reset() {
bitrateEstimate = ESTIMATE_NOT_AVAILABLE;
}
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.TimeToFirstByteEstimator;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import java.util.LinkedHashMap;
import java.util.Map;
/** Implementation of {@link TimeToFirstByteEstimator} based on exponential weighted average. */
public final class ExponentialWeightedAverageTimeToFirstByteEstimator
implements TimeToFirstByteEstimator {
/** The default smoothing factor. */
public static final double DEFAULT_SMOOTHING_FACTOR = 0.85;
private static final int MAX_DATA_SPECS = 10;
private final LinkedHashMap<DataSpec, Long> initializedDataSpecs;
private final double smoothingFactor;
private final Clock clock;
private long estimateUs;
/** Creates an instance using the {@link #DEFAULT_SMOOTHING_FACTOR}. */
public ExponentialWeightedAverageTimeToFirstByteEstimator() {
this(DEFAULT_SMOOTHING_FACTOR, Clock.DEFAULT);
}
/**
* Creates an instance.
*
* @param smoothingFactor The exponential weighted average smoothing factor.
*/
public ExponentialWeightedAverageTimeToFirstByteEstimator(double smoothingFactor) {
this(smoothingFactor, Clock.DEFAULT);
}
/**
* Creates an instance.
*
* @param smoothingFactor The exponential weighted average smoothing factor.
* @param clock The {@link Clock} used for calculating time samples.
*/
@VisibleForTesting
/* package */ ExponentialWeightedAverageTimeToFirstByteEstimator(
double smoothingFactor, Clock clock) {
this.smoothingFactor = smoothingFactor;
this.clock = clock;
initializedDataSpecs = new FixedSizeLinkedHashMap<>(/* maxSize= */ MAX_DATA_SPECS);
estimateUs = C.TIME_UNSET;
}
@Override
public long getTimeToFirstByteEstimateUs() {
return estimateUs;
}
@Override
public void reset() {
estimateUs = C.TIME_UNSET;
}
@Override
public void onTransferInitializing(DataSpec dataSpec) {
// Remove to make sure insertion order is updated in case the key already exists.
initializedDataSpecs.remove(dataSpec);
initializedDataSpecs.put(dataSpec, Util.msToUs(clock.elapsedRealtime()));
}
@Override
public void onTransferStart(DataSpec dataSpec) {
@Nullable Long initializationStartUs = initializedDataSpecs.remove(dataSpec);
if (initializationStartUs == null) {
return;
}
long timeToStartSampleUs = Util.msToUs(clock.elapsedRealtime()) - initializationStartUs;
if (estimateUs == C.TIME_UNSET) {
estimateUs = timeToStartSampleUs;
} else {
estimateUs =
(long) (smoothingFactor * estimateUs + (1d - smoothingFactor) * timeToStartSampleUs);
}
}
private static class FixedSizeLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public FixedSizeLinkedHashMap(int maxSize) {
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
}
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.SlidingPercentile;
import com.google.android.exoplayer2.upstream.TimeToFirstByteEstimator;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Implementation of {@link TimeToFirstByteEstimator} that returns a configured percentile of a
* sliding window of collected response times.
*/
public final class PercentileTimeToFirstByteEstimator implements TimeToFirstByteEstimator {
/** The default maximum number of samples. */
public static final int DEFAULT_MAX_SAMPLES_COUNT = 10;
/** The default percentile to return. */
public static final float DEFAULT_PERCENTILE = 0.5f;
private static final int MAX_DATA_SPECS = 10;
private final LinkedHashMap<DataSpec, Long> initializedDataSpecs;
private final SlidingPercentile slidingPercentile;
private final float percentile;
private final Clock clock;
private boolean isEmpty;
/**
* Creates an instance that keeps up to {@link #DEFAULT_MAX_SAMPLES_COUNT} samples and returns the
* {@link #DEFAULT_PERCENTILE} percentile.
*/
public PercentileTimeToFirstByteEstimator() {
this(DEFAULT_MAX_SAMPLES_COUNT, DEFAULT_PERCENTILE);
}
/**
* Creates an instance.
*
* @param numberOfSamples The maximum number of samples to be kept in the sliding window.
* @param percentile The percentile for estimating the time to the first byte.
*/
public PercentileTimeToFirstByteEstimator(int numberOfSamples, float percentile) {
this(numberOfSamples, percentile, Clock.DEFAULT);
}
/**
* Creates an instance.
*
* @param numberOfSamples The maximum number of samples to be kept in the sliding window.
* @param percentile The percentile for estimating the time to the first byte.
* @param clock The {@link Clock} to use.
*/
@VisibleForTesting
/* package */ PercentileTimeToFirstByteEstimator(
int numberOfSamples, float percentile, Clock clock) {
checkArgument(numberOfSamples > 0 && percentile > 0 && percentile <= 1);
this.percentile = percentile;
this.clock = clock;
initializedDataSpecs = new FixedSizeLinkedHashMap<>(/* maxSize= */ MAX_DATA_SPECS);
slidingPercentile = new SlidingPercentile(/* maxWeight= */ numberOfSamples);
isEmpty = true;
}
@Override
public long getTimeToFirstByteEstimateUs() {
return !isEmpty ? (long) slidingPercentile.getPercentile(percentile) : C.TIME_UNSET;
}
@Override
public void reset() {
slidingPercentile.reset();
isEmpty = true;
}
@Override
public void onTransferInitializing(DataSpec dataSpec) {
// Remove to make sure insertion order is updated in case the key already exists.
initializedDataSpecs.remove(dataSpec);
initializedDataSpecs.put(dataSpec, Util.msToUs(clock.elapsedRealtime()));
}
@Override
public void onTransferStart(DataSpec dataSpec) {
@Nullable Long initializationStartUs = initializedDataSpecs.remove(dataSpec);
if (initializationStartUs == null) {
return;
}
slidingPercentile.addSample(
/* weight= */ 1,
/* value= */ (float) (Util.msToUs(clock.elapsedRealtime()) - initializationStartUs));
isEmpty = false;
}
private static class FixedSizeLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public FixedSizeLinkedHashMap(int maxSize) {
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
}
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.android.exoplayer2.upstream.experimental.BandwidthEstimator.ESTIMATE_NOT_AVAILABLE;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque;
import java.util.TreeSet;
/**
* A {@link BandwidthStatistic} that calculates estimates based on a sliding window weighted
* percentile.
*/
public class SlidingPercentileBandwidthStatistic implements BandwidthStatistic {
/** The default maximum number of samples. */
public static final int DEFAULT_MAX_SAMPLES_COUNT = 10;
/** The default percentile to return. */
public static final double DEFAULT_PERCENTILE = 0.5;
private final int maxSampleCount;
private final double percentile;
private final ArrayDeque<Sample> samples;
private final TreeSet<Sample> sortedSamples;
private double weightSum;
private long bitrateEstimate;
/**
* Creates an instance with a maximum of {@link #DEFAULT_MAX_SAMPLES_COUNT} samples, returning the
* {@link #DEFAULT_PERCENTILE}.
*/
public SlidingPercentileBandwidthStatistic() {
this(DEFAULT_MAX_SAMPLES_COUNT, DEFAULT_PERCENTILE);
}
/**
* Creates an instance.
*
* @param maxSampleCount The maximum number of samples.
* @param percentile The percentile to return. Must be in the range of [0-1].
*/
public SlidingPercentileBandwidthStatistic(int maxSampleCount, double percentile) {
checkArgument(percentile >= 0 && percentile <= 1);
this.maxSampleCount = maxSampleCount;
this.percentile = percentile;
this.samples = new ArrayDeque<>();
this.sortedSamples = new TreeSet<>();
this.bitrateEstimate = ESTIMATE_NOT_AVAILABLE;
}
@Override
public void addSample(long bytes, long durationUs) {
while (samples.size() >= maxSampleCount) {
Sample removedSample = samples.remove();
sortedSamples.remove(removedSample);
weightSum -= removedSample.weight;
}
double weight = Math.sqrt((double) bytes);
long bitrate = bytes * 8_000_000 / durationUs;
Sample sample = new Sample(bitrate, weight);
samples.add(sample);
sortedSamples.add(sample);
weightSum += weight;
bitrateEstimate = calculateBitrateEstimate();
}
@Override
public long getBandwidthEstimate() {
return bitrateEstimate;
}
@Override
public void reset() {
samples.clear();
sortedSamples.clear();
weightSum = 0;
bitrateEstimate = ESTIMATE_NOT_AVAILABLE;
}
private long calculateBitrateEstimate() {
if (samples.isEmpty()) {
return ESTIMATE_NOT_AVAILABLE;
}
double targetWeightSum = weightSum * percentile;
double previousPartialWeightSum = 0;
long previousSampleBitrate = 0;
double nextPartialWeightSum = 0;
for (Sample sample : sortedSamples) {
// The percentile position of each sample is the middle of its weight. Hence, we need to add
// half the weight to check whether the target percentile is before or after this sample.
nextPartialWeightSum += sample.weight / 2;
if (nextPartialWeightSum >= targetWeightSum) {
if (previousSampleBitrate == 0) {
return sample.bitrate;
}
// Interpolate between samples to get an estimate for the target percentile.
double partialBitrateBetweenSamples =
(sample.bitrate - previousSampleBitrate)
* (targetWeightSum - previousPartialWeightSum)
/ (nextPartialWeightSum - previousPartialWeightSum);
return previousSampleBitrate + (long) partialBitrateBetweenSamples;
}
previousSampleBitrate = sample.bitrate;
previousPartialWeightSum = nextPartialWeightSum;
nextPartialWeightSum += sample.weight / 2;
}
return previousSampleBitrate;
}
private static class Sample implements Comparable<Sample> {
private final long bitrate;
private final double weight;
public Sample(long bitrate, double weight) {
this.bitrate = bitrate;
this.weight = weight;
}
@Override
public int compareTo(Sample other) {
return Util.compareLong(this.bitrate, other.bitrate);
}
}
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.util.Clock;
import java.util.ArrayDeque;
import java.util.Deque;
/**
* A {@link BandwidthStatistic} that calculates estimates based on a sliding window weighted
* average.
*/
public class SlidingWeightedAverageBandwidthStatistic implements BandwidthStatistic {
/** Represents a bandwidth sample. */
public static class Sample {
/** The sample bitrate. */
public final long bitrate;
/** The sample weight. */
public final double weight;
/**
* The time this sample was added, in milliseconds. Timestamps should come from the same source,
* so that samples can reliably be ordered in time. It is suggested to use {@link
* Clock#elapsedRealtime()}.
*/
public final long timeAddedMs;
/** Creates a new sample. */
public Sample(long bitrate, double weight, long timeAddedMs) {
this.bitrate = bitrate;
this.weight = weight;
this.timeAddedMs = timeAddedMs;
}
}
/** An interface to decide if samples need to be evicted from the estimator. */
public interface SampleEvictionFunction {
/**
* Whether the sample at the front of the queue needs to be evicted. Called before adding a next
* sample.
*
* @param samples A queue of samples, ordered by {@link Sample#timeAddedMs}. The oldest sample
* is at front of the queue. The queue must not be modified.
*/
boolean shouldEvictSample(Deque<Sample> samples);
}
/** Gets a {@link SampleEvictionFunction} that maintains up to {@code maxSamplesCount} samples. */
public static SampleEvictionFunction getMaxCountEvictionFunction(long maxSamplesCount) {
return (samples) -> samples.size() >= maxSamplesCount;
}
/** Gets a {@link SampleEvictionFunction} that maintains samples up to {@code maxAgeMs}. */
public static SampleEvictionFunction getAgeBasedEvictionFunction(long maxAgeMs) {
return getAgeBasedEvictionFunction(maxAgeMs, Clock.DEFAULT);
}
@VisibleForTesting
/* package */ static SampleEvictionFunction getAgeBasedEvictionFunction(
long maxAgeMs, Clock clock) {
return (samples) -> {
if (samples.isEmpty()) {
return false;
}
return castNonNull(samples.peek()).timeAddedMs + maxAgeMs < clock.elapsedRealtime();
};
}
/** The default maximum number of samples. */
public static final int DEFAULT_MAX_SAMPLES_COUNT = 10;
private final ArrayDeque<Sample> samples;
private final SampleEvictionFunction sampleEvictionFunction;
private final Clock clock;
private double bitrateWeightProductSum;
private double weightSum;
/** Creates an instance that keeps up to {@link #DEFAULT_MAX_SAMPLES_COUNT} samples. */
public SlidingWeightedAverageBandwidthStatistic() {
this(getMaxCountEvictionFunction(DEFAULT_MAX_SAMPLES_COUNT));
}
/**
* Creates an instance.
*
* @param sampleEvictionFunction The {@link SampleEvictionFunction} deciding whether to drop
* samples when new samples are added.
*/
public SlidingWeightedAverageBandwidthStatistic(SampleEvictionFunction sampleEvictionFunction) {
this(sampleEvictionFunction, Clock.DEFAULT);
}
/**
* Creates an instance.
*
* @param sampleEvictionFunction The {@link SampleEvictionFunction} deciding whether to drop
* samples when new samples are added.
* @param clock The {@link Clock} used.
*/
@VisibleForTesting
/* package */ SlidingWeightedAverageBandwidthStatistic(
SampleEvictionFunction sampleEvictionFunction, Clock clock) {
this.samples = new ArrayDeque<>();
this.sampleEvictionFunction = sampleEvictionFunction;
this.clock = clock;
}
@Override
public void addSample(long bytes, long durationUs) {
while (sampleEvictionFunction.shouldEvictSample(samples)) {
Sample sample = samples.remove();
bitrateWeightProductSum -= sample.bitrate * sample.weight;
weightSum -= sample.weight;
}
double weight = Math.sqrt((double) bytes);
long bitrate = bytes * 8_000_000 / durationUs;
Sample sample = new Sample(bitrate, weight, clock.elapsedRealtime());
samples.add(sample);
bitrateWeightProductSum += sample.bitrate * sample.weight;
weightSum += sample.weight;
}
@Override
public long getBandwidthEstimate() {
if (samples.isEmpty()) {
return BandwidthEstimator.ESTIMATE_NOT_AVAILABLE;
}
return (long) (bitrateWeightProductSum / weightSum);
}
@Override
public void reset() {
samples.clear();
bitrateWeightProductSum = 0;
weightSum = 0;
}
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.os.Handler;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Clock;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/**
* A {@link BandwidthEstimator} that captures a transfer sample each time a transfer ends. When
* parallel transfers are happening at the same time, the transferred bytes are aggregated in a
* single sample.
*/
public class SplitParallelSampleBandwidthEstimator implements BandwidthEstimator {
/** A builder to create {@link SplitParallelSampleBandwidthEstimator} instances. */
public static class Builder {
private BandwidthStatistic bandwidthStatistic;
private int minSamples;
private long minBytesTransferred;
private Clock clock;
/** Creates a new builder instance. */
public Builder() {
bandwidthStatistic = new SlidingWeightedAverageBandwidthStatistic();
clock = Clock.DEFAULT;
}
/**
* Sets the {@link BandwidthStatistic} to be used by the estimator. By default, this is set to a
* {@link SlidingWeightedAverageBandwidthStatistic}.
*
* @param bandwidthStatistic The {@link BandwidthStatistic}.
* @return This builder for convenience.
*/
@CanIgnoreReturnValue
public Builder setBandwidthStatistic(BandwidthStatistic bandwidthStatistic) {
checkNotNull(bandwidthStatistic);
this.bandwidthStatistic = bandwidthStatistic;
return this;
}
/**
* Sets a minimum threshold of samples that need to be taken before the estimator can return a
* bandwidth estimate. By default, this is set to {@code 0}.
*
* @param minSamples The minimum number of samples.
* @return This builder for convenience.
*/
@CanIgnoreReturnValue
public Builder setMinSamples(int minSamples) {
checkArgument(minSamples >= 0);
this.minSamples = minSamples;
return this;
}
/**
* Sets a minimum threshold of bytes that need to be transferred before the estimator can return
* a bandwidth estimate. By default, this is set to {@code 0}.
*
* @param minBytesTransferred The minimum number of transferred bytes.
* @return This builder for convenience.
*/
@CanIgnoreReturnValue
public Builder setMinBytesTransferred(long minBytesTransferred) {
checkArgument(minBytesTransferred >= 0);
this.minBytesTransferred = minBytesTransferred;
return this;
}
/**
* Sets the {@link Clock} used by the estimator. By default, this is set to {@link
* Clock#DEFAULT}.
*
* @param clock The {@link Clock} to be used.
* @return This builder for convenience.
*/
@CanIgnoreReturnValue
@VisibleForTesting
/* package */ Builder setClock(Clock clock) {
this.clock = clock;
return this;
}
public SplitParallelSampleBandwidthEstimator build() {
return new SplitParallelSampleBandwidthEstimator(this);
}
}
private final BandwidthStatistic bandwidthStatistic;
private final int minSamples;
private final long minBytesTransferred;
private final Clock clock;
private final BandwidthMeter.EventListener.EventDispatcher eventDispatcher;
private int streamCount;
private long sampleStartTimeMs;
private long sampleBytesTransferred;
private long bandwidthEstimate;
private long lastReportedBandwidthEstimate;
private int totalSamplesAdded;
private long totalBytesTransferred;
private SplitParallelSampleBandwidthEstimator(Builder builder) {
this.bandwidthStatistic = builder.bandwidthStatistic;
this.minSamples = builder.minSamples;
this.minBytesTransferred = builder.minBytesTransferred;
this.clock = builder.clock;
eventDispatcher = new BandwidthMeter.EventListener.EventDispatcher();
bandwidthEstimate = ESTIMATE_NOT_AVAILABLE;
lastReportedBandwidthEstimate = ESTIMATE_NOT_AVAILABLE;
}
@Override
public void addEventListener(Handler eventHandler, BandwidthMeter.EventListener eventListener) {
eventDispatcher.addListener(eventHandler, eventListener);
}
@Override
public void removeEventListener(BandwidthMeter.EventListener eventListener) {
eventDispatcher.removeListener(eventListener);
}
@Override
public void onTransferInitializing(DataSource source) {}
@Override
public void onTransferStart(DataSource source) {
if (streamCount == 0) {
sampleStartTimeMs = clock.elapsedRealtime();
}
streamCount++;
}
@Override
public void onBytesTransferred(DataSource source, int bytesTransferred) {
sampleBytesTransferred += bytesTransferred;
totalBytesTransferred += bytesTransferred;
}
@Override
public void onTransferEnd(DataSource source) {
checkState(streamCount > 0);
long nowMs = clock.elapsedRealtime();
long sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs);
if (sampleElapsedTimeMs > 0) {
bandwidthStatistic.addSample(sampleBytesTransferred, sampleElapsedTimeMs * 1000);
totalSamplesAdded++;
if (totalSamplesAdded > minSamples && totalBytesTransferred > minBytesTransferred) {
bandwidthEstimate = bandwidthStatistic.getBandwidthEstimate();
}
maybeNotifyBandwidthSample(
(int) sampleElapsedTimeMs, sampleBytesTransferred, bandwidthEstimate);
sampleStartTimeMs = nowMs;
sampleBytesTransferred = 0;
} // Else any sample bytes transferred will be carried forward into the next sample.
streamCount--;
}
@Override
public long getBandwidthEstimate() {
return bandwidthEstimate;
}
@Override
public void onNetworkTypeChange(long newBandwidthEstimate) {
long nowMs = clock.elapsedRealtime();
int sampleElapsedTimeMs = streamCount > 0 ? (int) (nowMs - sampleStartTimeMs) : 0;
maybeNotifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, newBandwidthEstimate);
bandwidthStatistic.reset();
bandwidthEstimate = ESTIMATE_NOT_AVAILABLE;
sampleStartTimeMs = nowMs;
sampleBytesTransferred = 0;
totalSamplesAdded = 0;
totalBytesTransferred = 0;
}
private void maybeNotifyBandwidthSample(
int elapsedMs, long bytesTransferred, long bandwidthEstimate) {
if ((bandwidthEstimate == ESTIMATE_NOT_AVAILABLE)
|| (elapsedMs == 0
&& bytesTransferred == 0
&& bandwidthEstimate == lastReportedBandwidthEstimate)) {
return;
}
lastReportedBandwidthEstimate = bandwidthEstimate;
eventDispatcher.bandwidthSample(elapsedMs, bytesTransferred, bandwidthEstimate);
}
}
/*
* Copyright (C) 2021 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.
*/
@NonNullApi
package com.google.android.exoplayer2.upstream.experimental;
import com.google.android.exoplayer2.util.NonNullApi;
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.os.Handler;
import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.shadows.ShadowLooper;
/** Unite tests for the {@link CombinedParallelSampleBandwidthEstimator}. */
@RunWith(AndroidJUnit4.class)
public class CombinedParallelSampleBandwidthEstimatorTest {
@Test
public void builder_setNegativeMinSamples_throws() {
assertThrows(
IllegalArgumentException.class,
() -> new CombinedParallelSampleBandwidthEstimator.Builder().setMinSamples(-1));
}
@Test
public void builder_setNegativeMinBytesTransferred_throws() {
assertThrows(
IllegalArgumentException.class,
() -> new CombinedParallelSampleBandwidthEstimator.Builder().setMinBytesTransferred(-1));
}
@Test
public void transferEvents_singleTransfer_providesOneSample() {
FakeClock fakeClock = new FakeClock(0);
CombinedParallelSampleBandwidthEstimator estimator =
new CombinedParallelSampleBandwidthEstimator.Builder().setClock(fakeClock).build();
BandwidthMeter.EventListener eventListener = Mockito.mock(BandwidthMeter.EventListener.class);
estimator.addEventListener(new Handler(Looper.getMainLooper()), eventListener);
DataSource source = new FakeDataSource();
estimator.onTransferInitializing(source);
fakeClock.advanceTime(10);
estimator.onTransferStart(source);
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source, /* bytesTransferred= */ 200);
fakeClock.advanceTime(10);
estimator.onTransferEnd(source);
ShadowLooper.idleMainLooper();
assertThat(estimator.getBandwidthEstimate()).isEqualTo(80_000);
verify(eventListener).onBandwidthSample(20, 200, 80_000);
}
@Test
public void transferEvents_twoParallelTransfers_providesOneSample() {
FakeClock fakeClock = new FakeClock(0);
CombinedParallelSampleBandwidthEstimator estimator =
new CombinedParallelSampleBandwidthEstimator.Builder().setClock(fakeClock).build();
BandwidthMeter.EventListener eventListener = Mockito.mock(BandwidthMeter.EventListener.class);
estimator.addEventListener(new Handler(Looper.getMainLooper()), eventListener);
DataSource source1 = new FakeDataSource();
DataSource source2 = new FakeDataSource();
// At time = 10 ms, source1 starts.
fakeClock.advanceTime(10);
estimator.onTransferInitializing(source1);
estimator.onTransferStart(source1);
// At time 20 ms, source1 reports 200 bytes.
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source1, /* bytesTransferred= */ 200);
// At time = 30 ms, source2 starts.
fakeClock.advanceTime(10);
estimator.onTransferInitializing(source2);
estimator.onTransferStart(source2);
// At time = 40 ms, both sources report 100 bytes each.
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source1, /* bytesTransferred= */ 100);
estimator.onBytesTransferred(source2, /* bytesTransferred= */ 100);
// At time = 50 ms, source1 transfer completes. At this point, 400 bytes have been transferred
// in total between times 10 and 50 ms.
fakeClock.advanceTime(10);
estimator.onTransferEnd(source1);
ShadowLooper.idleMainLooper();
// Verify no update has been made yet.
verify(eventListener, never()).onBandwidthSample(anyInt(), anyLong(), anyLong());
// At time = 60 ms, source2 reports 160 bytes.
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source2, /* bytesTransferred= */ 160);
// At time = 70 ms second transfer completes. At this time, 160 bytes have been
// transferred between times 50 and 70 ms.
fakeClock.advanceTime(10);
estimator.onTransferEnd(source2);
ShadowLooper.idleMainLooper();
assertThat(estimator.getBandwidthEstimate()).isEqualTo(74_666);
verify(eventListener).onBandwidthSample(60, 560, 74_666);
verifyNoMoreInteractions(eventListener);
}
@Test
public void onNetworkTypeChange_notifiesListener() {
FakeClock fakeClock = new FakeClock(0);
CombinedParallelSampleBandwidthEstimator estimator =
new CombinedParallelSampleBandwidthEstimator.Builder().setClock(fakeClock).build();
BandwidthMeter.EventListener eventListener = Mockito.mock(BandwidthMeter.EventListener.class);
estimator.addEventListener(new Handler(Looper.getMainLooper()), eventListener);
estimator.onNetworkTypeChange(100);
ShadowLooper.idleMainLooper();
verify(eventListener).onBandwidthSample(0, 0, 100);
}
@Test
public void minSamplesSet_doesNotReturnEstimateBefore() {
FakeDataSource source = new FakeDataSource();
FakeClock fakeClock = new FakeClock(0);
BandwidthStatistic mockStatistic = mock(BandwidthStatistic.class);
when(mockStatistic.getBandwidthEstimate()).thenReturn(1234L);
CombinedParallelSampleBandwidthEstimator estimator =
new CombinedParallelSampleBandwidthEstimator.Builder()
.setBandwidthStatistic(mockStatistic)
.setMinSamples(1)
.setClock(fakeClock)
.build();
// First sample.
estimator.onTransferInitializing(source);
estimator.onTransferStart(source);
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source, /* bytesTransferred= */ 100);
fakeClock.advanceTime(10);
estimator.onTransferEnd(source);
assertThat(estimator.getBandwidthEstimate())
.isEqualTo(BandwidthEstimator.ESTIMATE_NOT_AVAILABLE);
// Second sample.
fakeClock.advanceTime(10);
estimator.onTransferInitializing(source);
estimator.onTransferStart(source);
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source, /* bytesTransferred= */ 100);
fakeClock.advanceTime(10);
estimator.onTransferEnd(source);
assertThat(estimator.getBandwidthEstimate()).isEqualTo(1234L);
}
@Test
public void minBytesTransferredSet_doesNotReturnEstimateBefore() {
FakeDataSource source = new FakeDataSource();
FakeClock fakeClock = new FakeClock(0);
BandwidthStatistic mockStatistic = mock(BandwidthStatistic.class);
when(mockStatistic.getBandwidthEstimate()).thenReturn(1234L);
CombinedParallelSampleBandwidthEstimator estimator =
new CombinedParallelSampleBandwidthEstimator.Builder()
.setBandwidthStatistic(mockStatistic)
.setMinBytesTransferred(500)
.setClock(fakeClock)
.build();
// First sample transfers 499 bytes.
estimator.onTransferInitializing(source);
estimator.onTransferStart(source);
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source, /* bytesTransferred= */ 499);
fakeClock.advanceTime(10);
estimator.onTransferEnd(source);
assertThat(estimator.getBandwidthEstimate())
.isEqualTo(BandwidthEstimator.ESTIMATE_NOT_AVAILABLE);
// Second sample transfers 100 bytes.
fakeClock.advanceTime(10);
estimator.onTransferInitializing(source);
estimator.onTransferStart(source);
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source, /* bytesTransferred= */ 100);
fakeClock.advanceTime(10);
estimator.onTransferEnd(source);
assertThat(estimator.getBandwidthEstimate()).isEqualTo(1234L);
}
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link ExponentialWeightedAverageStatistic}. */
@RunWith(AndroidJUnit4.class)
public class ExponentialWeightedAverageStatisticTest {
@Test
public void getBandwidthEstimate_afterConstruction_returnsNoEstimate() {
ExponentialWeightedAverageStatistic statistic = new ExponentialWeightedAverageStatistic();
assertThat(statistic.getBandwidthEstimate())
.isEqualTo(BandwidthEstimator.ESTIMATE_NOT_AVAILABLE);
}
@Test
public void getBandwidthEstimate_oneSample_returnsEstimate() {
ExponentialWeightedAverageStatistic statistic = new ExponentialWeightedAverageStatistic();
statistic.addSample(/* bytes= */ 10, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(8_000_000);
}
@Test
public void getBandwidthEstimate_multipleSamples_returnsEstimate() {
ExponentialWeightedAverageStatistic statistic =
new ExponentialWeightedAverageStatistic(/* smoothingFactor= */ 0.9999);
// Transfer bytes are chosen so that their weights (square root) is exactly an integer.
statistic.addSample(/* bytes= */ 400, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 100, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 64, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(319545334);
}
@Test
public void getBandwidthEstimate_calledMultipleTimes_returnsSameEstimate() {
ExponentialWeightedAverageStatistic statistic =
new ExponentialWeightedAverageStatistic(/* smoothingFactor= */ 0.9999);
// Transfer bytes chosen so that their weight (sqrt) is an integer.
statistic.addSample(/* bytes= */ 400, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 100, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 64, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(319545334);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(319545334);
}
@Test
public void reset_withSamplesAdded_returnsNoEstimate() {
ExponentialWeightedAverageStatistic statistic = new ExponentialWeightedAverageStatistic();
statistic.addSample(/* bytes= */ 10, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 10, /* durationUs= */ 10);
statistic.reset();
assertThat(statistic.getBandwidthEstimate())
.isEqualTo(BandwidthEstimator.ESTIMATE_NOT_AVAILABLE);
}
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.android.exoplayer2.upstream.experimental.ExponentialWeightedAverageTimeToFirstByteEstimator.DEFAULT_SMOOTHING_FACTOR;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.upstream.DataSpec;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link ExponentialWeightedAverageTimeToFirstByteEstimator}. */
@RunWith(AndroidJUnit4.class)
public class ExponentialWeightedAverageTimeToFirstByteEstimatorTest {
@Test
public void timeToFirstByteEstimate_afterConstruction_notAvailable() {
ExponentialWeightedAverageTimeToFirstByteEstimator estimator =
new ExponentialWeightedAverageTimeToFirstByteEstimator();
assertThat(estimator.getTimeToFirstByteEstimateUs()).isEqualTo(C.TIME_UNSET);
}
@Test
public void timeToFirstByteEstimate_afterReset_notAvailable() {
FakeClock clock = new FakeClock(0);
ExponentialWeightedAverageTimeToFirstByteEstimator estimator =
new ExponentialWeightedAverageTimeToFirstByteEstimator(DEFAULT_SMOOTHING_FACTOR, clock);
DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
// Initialize and start two transfers.
estimator.onTransferInitializing(dataSpec);
clock.advanceTime(10);
estimator.onTransferStart(dataSpec);
// Second transfer.
estimator.onTransferInitializing(dataSpec);
clock.advanceTime(10);
estimator.onTransferStart(dataSpec);
assertThat(estimator.getTimeToFirstByteEstimateUs()).isGreaterThan(0);
estimator.reset();
assertThat(estimator.getTimeToFirstByteEstimateUs()).isEqualTo(C.TIME_UNSET);
}
@Test
public void timeToFirstByteEstimate_afterTwoSamples_returnsEstimate() {
FakeClock clock = new FakeClock(0);
ExponentialWeightedAverageTimeToFirstByteEstimator estimator =
new ExponentialWeightedAverageTimeToFirstByteEstimator(DEFAULT_SMOOTHING_FACTOR, clock);
DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
// Initialize and start two transfers.
estimator.onTransferInitializing(dataSpec);
clock.advanceTime(10);
estimator.onTransferStart(dataSpec);
// Second transfer.
estimator.onTransferInitializing(dataSpec);
clock.advanceTime(5);
estimator.onTransferStart(dataSpec);
// (0.85 * 10ms) + (0.15 * 5ms) = 9.25ms => 9250us
assertThat(estimator.getTimeToFirstByteEstimateUs()).isEqualTo(9250);
}
@Test
public void timeToFirstByteEstimate_withUserDefinedSmoothingFactor_returnsEstimate() {
FakeClock clock = new FakeClock(0);
ExponentialWeightedAverageTimeToFirstByteEstimator estimator =
new ExponentialWeightedAverageTimeToFirstByteEstimator(/* smoothingFactor= */ 0.9, clock);
DataSpec dataSpec = new DataSpec.Builder().setUri(Uri.EMPTY).build();
// Initialize and start two transfers.
estimator.onTransferInitializing(dataSpec);
clock.advanceTime(10);
estimator.onTransferStart(dataSpec);
// Second transfer.
estimator.onTransferInitializing(dataSpec);
clock.advanceTime(5);
estimator.onTransferStart(dataSpec);
// (0.9 * 10ms) + (0.1 * 5ms) = 9.5ms => 9500 us
assertThat(estimator.getTimeToFirstByteEstimateUs()).isEqualTo(9500);
}
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import java.time.Duration;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowSystemClock;
/** Unit tests for {@link PercentileTimeToFirstByteEstimator}. */
@RunWith(AndroidJUnit4.class)
public class PercentileTimeToFirstByteEstimatorTest {
private PercentileTimeToFirstByteEstimator percentileTimeToResponseEstimator;
@Before
public void setUp() {
percentileTimeToResponseEstimator =
new PercentileTimeToFirstByteEstimator(/* numberOfSamples= */ 5, /* percentile= */ 0.5f);
}
@Test
public void constructor_invalidNumberOfSamples_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class,
() ->
new PercentileTimeToFirstByteEstimator(
/* numberOfSamples= */ 0, /* percentile= */ .2f));
assertThrows(
IllegalArgumentException.class,
() ->
new PercentileTimeToFirstByteEstimator(
/* numberOfSamples= */ -123, /* percentile= */ .2f));
}
@Test
public void constructor_invalidPercentile_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class,
() ->
new PercentileTimeToFirstByteEstimator(
/* numberOfSamples= */ 11, /* percentile= */ .0f));
assertThrows(
IllegalArgumentException.class,
() ->
new PercentileTimeToFirstByteEstimator(
/* numberOfSamples= */ 11, /* percentile= */ 1.1f));
}
@Test
public void getTimeToRespondEstimateUs_noSamples_returnsTimeUnset() {
assertThat(percentileTimeToResponseEstimator.getTimeToFirstByteEstimateUs())
.isEqualTo(C.TIME_UNSET);
}
@Test
public void getTimeToRespondEstimateUs_medianOfOddNumberOfSamples_returnsCenterSampleValue() {
DataSpec dataSpec = new DataSpec(Uri.EMPTY);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(10));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(20));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(30));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(40));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(50));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
assertThat(percentileTimeToResponseEstimator.getTimeToFirstByteEstimateUs()).isEqualTo(30_000);
}
@Test
public void
getTimeToRespondEstimateUs_medianOfEvenNumberOfSamples_returnsLastSampleOfFirstHalfValue() {
PercentileTimeToFirstByteEstimator percentileTimeToResponseEstimator =
new PercentileTimeToFirstByteEstimator(/* numberOfSamples= */ 12, /* percentile= */ 0.5f);
DataSpec dataSpec = new DataSpec(Uri.EMPTY);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(10));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(20));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(30));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(40));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
assertThat(percentileTimeToResponseEstimator.getTimeToFirstByteEstimateUs()).isEqualTo(20_000);
}
@Test
public void getTimeToRespondEstimateUs_slidingMedian_returnsCenterSampleValue() {
DataSpec dataSpec = new DataSpec(Uri.EMPTY);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(10));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(20));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(30));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(40));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(50));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(60));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(70));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
assertThat(percentileTimeToResponseEstimator.getTimeToFirstByteEstimateUs()).isEqualTo(50_000);
}
@Test
public void reset_clearsTheSlidingWindows() {
DataSpec dataSpec = new DataSpec(Uri.EMPTY);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(10));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.onTransferInitializing(dataSpec);
ShadowSystemClock.advanceBy(Duration.ofMillis(10));
percentileTimeToResponseEstimator.onTransferStart(dataSpec);
percentileTimeToResponseEstimator.reset();
assertThat(percentileTimeToResponseEstimator.getTimeToFirstByteEstimateUs())
.isEqualTo(C.TIME_UNSET);
}
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link SlidingPercentileBandwidthStatistic}. */
@RunWith(AndroidJUnit4.class)
public class SlidingPercentileBandwidthStatisticTest {
@Test
public void getBandwidthEstimate_afterConstruction_returnsNoEstimate() {
SlidingPercentileBandwidthStatistic statistic = new SlidingPercentileBandwidthStatistic();
assertThat(statistic.getBandwidthEstimate())
.isEqualTo(BandwidthEstimator.ESTIMATE_NOT_AVAILABLE);
}
@Test
public void getBandwidthEstimate_oneSample_returnsEstimate() {
SlidingPercentileBandwidthStatistic statistic =
new SlidingPercentileBandwidthStatistic(/* maxSampleCount= */ 10, /* percentile= */ 0.5);
statistic.addSample(/* bytes= */ 10, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(8_000_000);
}
@Test
public void getBandwidthEstimate_multipleSamples_returnsEstimate() {
SlidingPercentileBandwidthStatistic statistic =
new SlidingPercentileBandwidthStatistic(/* maxSampleCount= */ 10, /* percentile= */ 0.5);
// Transfer bytes are chosen so that their weights (square root) is exactly an integer.
statistic.addSample(/* bytes= */ 400, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 100, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 64, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(176_000_000);
}
@Test
public void getBandwidthEstimate_calledMultipleTimes_returnsSameEstimate() {
SlidingPercentileBandwidthStatistic statistic =
new SlidingPercentileBandwidthStatistic(/* maxSampleCount= */ 10, /* percentile= */ 0.5);
// Transfer bytes chosen so that their weight (sqrt) is an integer.
statistic.addSample(/* bytes= */ 400, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 100, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 64, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(176_000_000);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(176_000_000);
}
@Test
public void getBandwidthEstimate_afterMoreSamplesThanMaxSamples_usesOnlyMaxSamplesForEstimate() {
SlidingPercentileBandwidthStatistic statistic =
new SlidingPercentileBandwidthStatistic(/* maxSampleCount= */ 10, /* percentile= */ 0.5);
// Add 12 samples, the first two should be discarded
statistic.addSample(/* bytes= */ 1_000, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 1_000, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 16, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 16, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 16, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 16, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 16, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 16, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 16, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 16, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 16, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 16, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(12_800_000);
}
@Test
public void getBandwidthEstimate_nonMediaPercentile_returnsEstimate() {
SlidingPercentileBandwidthStatistic statistic =
new SlidingPercentileBandwidthStatistic(/* maxSampleCount= */ 10, /* percentile= */ 0.125);
// Transfer bytes are chosen so that their weights (square root) is exactly an integer.
statistic.addSample(/* bytes= */ 484, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 100, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 64, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(54_400_000);
}
@Test
public void reset_withSamplesAdded_returnsNoEstimate() {
SlidingPercentileBandwidthStatistic statistic =
new SlidingPercentileBandwidthStatistic(/* maxSampleCount= */ 10, /* percentile= */ 0.5);
statistic.addSample(/* bytes= */ 10, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 10, /* durationUs= */ 10);
statistic.reset();
assertThat(statistic.getBandwidthEstimate())
.isEqualTo(BandwidthEstimator.ESTIMATE_NOT_AVAILABLE);
}
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.FakeClock;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link SlidingWeightedAverageBandwidthStatistic}. */
@RunWith(AndroidJUnit4.class)
public class SlidingWeightedAverageBandwidthStatisticTest {
@Test
public void getBandwidthEstimate_afterConstruction_returnsNoEstimate() {
SlidingWeightedAverageBandwidthStatistic statistic =
new SlidingWeightedAverageBandwidthStatistic();
assertThat(statistic.getBandwidthEstimate())
.isEqualTo(BandwidthEstimator.ESTIMATE_NOT_AVAILABLE);
}
@Test
public void getBandwidthEstimate_oneSample_returnsEstimate() {
SlidingWeightedAverageBandwidthStatistic statistic =
new SlidingWeightedAverageBandwidthStatistic();
statistic.addSample(/* bytes= */ 10, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(8_000_000);
}
@Test
public void getBandwidthEstimate_multipleSamples_returnsEstimate() {
SlidingWeightedAverageBandwidthStatistic statistic =
new SlidingWeightedAverageBandwidthStatistic();
// Transfer bytes are chosen so that their weights (square root) is exactly an integer.
statistic.addSample(/* bytes= */ 400, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 100, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 64, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(200252631);
}
@Test
public void getBandwidthEstimate_calledMultipleTimes_returnsSameEstimate() {
SlidingWeightedAverageBandwidthStatistic statistic =
new SlidingWeightedAverageBandwidthStatistic();
// Transfer bytes chosen so that their weight (sqrt) is an integer.
statistic.addSample(/* bytes= */ 400, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 100, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 64, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(200_252_631);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(200_252_631);
}
@Test
public void defaultConstructor_estimatorKeepsTenSamples() {
SlidingWeightedAverageBandwidthStatistic statistic =
new SlidingWeightedAverageBandwidthStatistic();
// Add 12 samples, the first two should be discarded
statistic.addSample(/* bytes= */ 4, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 9, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 16, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 25, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 36, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 49, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 64, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 81, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 100, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 121, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 144, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 169, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(77_600_000);
}
@Test
public void constructorSetsMaxSamples_estimatorKeepsDefinedSamples() {
FakeClock fakeClock = new FakeClock(0);
SlidingWeightedAverageBandwidthStatistic statistic =
new SlidingWeightedAverageBandwidthStatistic(
SlidingWeightedAverageBandwidthStatistic.getMaxCountEvictionFunction(2), fakeClock);
statistic.addSample(/* bytes= */ 10, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 10, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 5, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 5, /* durationUs= */ 10);
assertThat(statistic.getBandwidthEstimate()).isEqualTo(4_000_000);
}
@Test
public void reset_withSamplesAdded_returnsNoEstimate() {
SlidingWeightedAverageBandwidthStatistic statistic =
new SlidingWeightedAverageBandwidthStatistic();
statistic.addSample(/* bytes= */ 10, /* durationUs= */ 10);
statistic.addSample(/* bytes= */ 10, /* durationUs= */ 10);
statistic.reset();
assertThat(statistic.getBandwidthEstimate())
.isEqualTo(BandwidthEstimator.ESTIMATE_NOT_AVAILABLE);
}
@Test
public void ageBasedSampleEvictionFunction_dropsOldSamples() {
// Create an estimator that keeps samples up to 15 seconds old.
FakeClock fakeClock = new FakeClock(0);
SlidingWeightedAverageBandwidthStatistic estimator =
new SlidingWeightedAverageBandwidthStatistic(
SlidingWeightedAverageBandwidthStatistic.getAgeBasedEvictionFunction(15_000),
fakeClock);
// Add sample at time = 0.99 seconds.
fakeClock.advanceTime(999);
estimator.addSample(/* bytes= */ 10, /* durationUs= */ 10);
// Add sample at time = 1 seconds.
fakeClock.advanceTime(1);
estimator.addSample(/* bytes= */ 5, /* durationUs= */ 10);
// Add sample at time = 5 seconds.
fakeClock.advanceTime(4_000);
estimator.addSample(/* bytes= */ 5, /* durationUs= */ 10);
// Add sample at time = 16 seconds, first sample should be dropped, but second sample should
// remain.
fakeClock.advanceTime(11_000);
estimator.addSample(/* bytes= */ 5, /* durationUs= */ 10);
assertThat(estimator.getBandwidthEstimate()).isEqualTo(4_000_000);
}
@Test
public void ageBasedSampleEvictionFunction_dropsOldSamples_onlyWhenAddingSamples() {
// Create an estimator that keeps samples up to 5 seconds old.
FakeClock fakeClock = new FakeClock(0);
SlidingWeightedAverageBandwidthStatistic estimator =
new SlidingWeightedAverageBandwidthStatistic(
SlidingWeightedAverageBandwidthStatistic.getAgeBasedEvictionFunction(5_000), fakeClock);
// Add sample at time = 0 seconds.
estimator.addSample(/* bytes= */ 16, /* durationUs= */ 10);
// Add sample at time = 4 seconds.
fakeClock.advanceTime(4_000);
estimator.addSample(/* bytes= */ 9, /* durationUs= */ 10);
// Advance clock to 10 seconds, samples should remain
fakeClock.advanceTime(6_000);
assertThat(estimator.getBandwidthEstimate()).isEqualTo(10_400_000);
}
}
/*
* Copyright 2021 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.exoplayer2.upstream.experimental;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.os.Handler;
import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowLooper;
/** Unite tests for the {@link SplitParallelSampleBandwidthEstimator}. */
@RunWith(AndroidJUnit4.class)
public class SplitParallelSampleBandwidthEstimatorTest {
@Test
public void builder_setNegativeMinSamples_throws() {
assertThrows(
IllegalArgumentException.class,
() -> new SplitParallelSampleBandwidthEstimator.Builder().setMinSamples(-1));
}
@Test
public void builder_setNegativeMinBytesTransferred_throws() {
assertThrows(
IllegalArgumentException.class,
() -> new SplitParallelSampleBandwidthEstimator.Builder().setMinBytesTransferred(-1));
}
@Test
public void transferEvents_singleTransfer_providesOneSample() {
FakeClock fakeClock = new FakeClock(0);
SplitParallelSampleBandwidthEstimator estimator =
new SplitParallelSampleBandwidthEstimator.Builder().setClock(fakeClock).build();
BandwidthMeter.EventListener eventListener = mock(BandwidthMeter.EventListener.class);
estimator.addEventListener(new Handler(Looper.getMainLooper()), eventListener);
DataSource source = new FakeDataSource();
estimator.onTransferInitializing(source);
fakeClock.advanceTime(10);
estimator.onTransferStart(source);
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source, /* bytesTransferred= */ 200);
fakeClock.advanceTime(10);
estimator.onTransferEnd(source);
ShadowLooper.idleMainLooper();
assertThat(estimator.getBandwidthEstimate()).isEqualTo(80_000);
verify(eventListener).onBandwidthSample(20, 200, 80_000);
}
@Test
public void transferEvents_twoParallelTransfers_providesTwoSamples() {
FakeClock fakeClock = new FakeClock(0);
SplitParallelSampleBandwidthEstimator estimator =
new SplitParallelSampleBandwidthEstimator.Builder().setClock(fakeClock).build();
BandwidthMeter.EventListener eventListener = mock(BandwidthMeter.EventListener.class);
estimator.addEventListener(new Handler(Looper.getMainLooper()), eventListener);
DataSource source1 = new FakeDataSource();
DataSource source2 = new FakeDataSource();
// At time = 10 ms, source1 starts.
fakeClock.advanceTime(10);
estimator.onTransferInitializing(source1);
estimator.onTransferStart(source1);
// At time 20 ms, source1 reports 200 bytes.
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source1, /* bytesTransferred= */ 200);
// At time = 30 ms, source2 starts.
fakeClock.advanceTime(10);
estimator.onTransferInitializing(source2);
estimator.onTransferStart(source2);
// At time = 40 ms, both sources report 100 bytes each.
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source1, /* bytesTransferred= */ 100);
estimator.onBytesTransferred(source2, /* bytesTransferred= */ 100);
// At time = 50 ms, source1 transfer completes. At this point, 400 bytes have been transferred
// in total between times 10 and 50 ms.
fakeClock.advanceTime(10);
estimator.onTransferEnd(source1);
ShadowLooper.idleMainLooper();
assertThat(estimator.getBandwidthEstimate()).isEqualTo(80_000);
verify(eventListener).onBandwidthSample(40, 400, 80_000);
// At time = 60 ms, source2 reports 160 bytes.
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source2, /* bytesTransferred= */ 160);
// At time = 70 ms second transfer completes. At this time, 160 bytes have been
// transferred between times 50 and 70 ms.
fakeClock.advanceTime(10);
estimator.onTransferEnd(source2);
ShadowLooper.idleMainLooper();
assertThat(estimator.getBandwidthEstimate()).isEqualTo(73_801);
verify(eventListener).onBandwidthSample(20, 160, 73_801);
}
@Test
public void onNetworkTypeChange_notifiesListener() {
FakeClock fakeClock = new FakeClock(0);
SplitParallelSampleBandwidthEstimator estimator =
new SplitParallelSampleBandwidthEstimator.Builder().setClock(fakeClock).build();
BandwidthMeter.EventListener eventListener = mock(BandwidthMeter.EventListener.class);
estimator.addEventListener(new Handler(Looper.getMainLooper()), eventListener);
estimator.onNetworkTypeChange(100);
ShadowLooper.idleMainLooper();
verify(eventListener).onBandwidthSample(0, 0, 100);
}
@Test
public void minSamplesSet_doesNotReturnEstimateBefore() {
FakeDataSource source = new FakeDataSource();
FakeClock fakeClock = new FakeClock(0);
BandwidthStatistic mockStatistic = mock(BandwidthStatistic.class);
when(mockStatistic.getBandwidthEstimate()).thenReturn(1234L);
SplitParallelSampleBandwidthEstimator estimator =
new SplitParallelSampleBandwidthEstimator.Builder()
.setBandwidthStatistic(mockStatistic)
.setMinSamples(1)
.setClock(fakeClock)
.build();
// First sample.
estimator.onTransferInitializing(source);
estimator.onTransferStart(source);
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source, /* bytesTransferred= */ 100);
fakeClock.advanceTime(10);
estimator.onTransferEnd(source);
assertThat(estimator.getBandwidthEstimate())
.isEqualTo(BandwidthEstimator.ESTIMATE_NOT_AVAILABLE);
// Second sample.
fakeClock.advanceTime(10);
estimator.onTransferInitializing(source);
estimator.onTransferStart(source);
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source, /* bytesTransferred= */ 100);
fakeClock.advanceTime(10);
estimator.onTransferEnd(source);
assertThat(estimator.getBandwidthEstimate()).isEqualTo(1234L);
}
@Test
public void minBytesTransferredSet_doesNotReturnEstimateBefore() {
FakeDataSource source = new FakeDataSource();
FakeClock fakeClock = new FakeClock(0);
BandwidthStatistic mockStatistic = mock(BandwidthStatistic.class);
when(mockStatistic.getBandwidthEstimate()).thenReturn(1234L);
SplitParallelSampleBandwidthEstimator estimator =
new SplitParallelSampleBandwidthEstimator.Builder()
.setBandwidthStatistic(mockStatistic)
.setMinBytesTransferred(500)
.setClock(fakeClock)
.build();
// First sample transfers 499 bytes.
estimator.onTransferInitializing(source);
estimator.onTransferStart(source);
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source, /* bytesTransferred= */ 499);
fakeClock.advanceTime(10);
estimator.onTransferEnd(source);
assertThat(estimator.getBandwidthEstimate())
.isEqualTo(BandwidthEstimator.ESTIMATE_NOT_AVAILABLE);
// Second sample transfers 100 bytes.
fakeClock.advanceTime(10);
estimator.onTransferInitializing(source);
estimator.onTransferStart(source);
fakeClock.advanceTime(10);
estimator.onBytesTransferred(source, /* bytesTransferred= */ 100);
fakeClock.advanceTime(10);
estimator.onTransferEnd(source);
assertThat(estimator.getBandwidthEstimate()).isEqualTo(1234L);
}
}
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