Publish experimental bandwidth meter classes

PiperOrigin-RevId: 524846153
This commit is contained in:
christosts 2023-04-17 15:53:52 +01:00 committed by Rohit Singh
parent e0bb23d463
commit 9081c70788
19 changed files with 3812 additions and 0 deletions

View file

@ -0,0 +1,85 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import android.os.Handler;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
/** The interface for different bandwidth estimation strategies. */
@UnstableApi
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);
}

View file

@ -0,0 +1,43 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import androidx.media3.common.util.UnstableApi;
/** The interface for different bandwidth estimation statistics. */
@UnstableApi
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();
}

View file

@ -0,0 +1,212 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import android.os.Handler;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/**
* A {@link BandwidthEstimator} that captures a transfer sample each time all parallel transfers
* end.
*/
@UnstableApi
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);
}
}

View file

@ -0,0 +1,834 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.content.Context;
import android.os.Handler;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.NetworkTypeObserver;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
import androidx.media3.exoplayer.upstream.TimeToFirstByteEstimator;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.HashMap;
import java.util.Map;
/**
* An experimental {@link BandwidthMeter} that estimates bandwidth by listening to data transfers.
*
* <p>The initial estimate is based on the current operator's network country code or the locale of
* the user, as well as the network connection type. This can be configured in the {@link Builder}.
*/
@UnstableApi
public final class ExperimentalBandwidthMeter implements BandwidthMeter, TransferListener {
/** Default initial Wifi bitrate estimate in bits per second. */
public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI =
ImmutableList.of(4_400_000L, 3_200_000L, 2_300_000L, 1_600_000L, 810_000L);
/** Default initial 2G bitrate estimates in bits per second. */
public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_2G =
ImmutableList.of(1_400_000L, 990_000L, 730_000L, 510_000L, 230_000L);
/** Default initial 3G bitrate estimates in bits per second. */
public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_3G =
ImmutableList.of(2_100_000L, 1_400_000L, 1_000_000L, 890_000L, 640_000L);
/** Default initial 4G bitrate estimates in bits per second. */
public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_4G =
ImmutableList.of(2_600_000L, 1_700_000L, 1_300_000L, 1_000_000L, 700_000L);
/** Default initial 5G-NSA bitrate estimates in bits per second. */
public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_NSA =
ImmutableList.of(5_700_000L, 3_700_000L, 2_300_000L, 1_700_000L, 990_000L);
/** Default initial 5G-SA bitrate estimates in bits per second. */
public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_SA =
ImmutableList.of(2_800_000L, 1_800_000L, 1_400_000L, 1_100_000L, 870_000L);
/**
* Default number of samples to keep in the sliding window for estimating the time to first byte.
*/
public static final int DEFAULT_TIME_TO_FIRST_BYTE_SAMPLES = 20;
/** Default percentile for estimating the time to first byte. */
public static final float DEFAULT_TIME_TO_FIRST_BYTE_PERCENTILE = 0.5f;
/**
* Default initial bitrate estimate used when the device is offline or the network type cannot be
* determined, in bits per second.
*/
public static final long DEFAULT_INITIAL_BITRATE_ESTIMATE = 1_000_000;
/**
* Index for the Wifi group index in the array returned by {@link
* #getInitialBitrateCountryGroupAssignment}.
*/
private static final int COUNTRY_GROUP_INDEX_WIFI = 0;
/**
* Index for the 2G group index in the array returned by {@link
* #getInitialBitrateCountryGroupAssignment}.
*/
private static final int COUNTRY_GROUP_INDEX_2G = 1;
/**
* Index for the 3G group index in the array returned by {@link
* #getInitialBitrateCountryGroupAssignment}.
*/
private static final int COUNTRY_GROUP_INDEX_3G = 2;
/**
* Index for the 4G group index in the array returned by {@link
* #getInitialBitrateCountryGroupAssignment}.
*/
private static final int COUNTRY_GROUP_INDEX_4G = 3;
/**
* Index for the 5G-NSA group index in the array returned by {@link
* #getInitialBitrateCountryGroupAssignment}.
*/
private static final int COUNTRY_GROUP_INDEX_5G_NSA = 4;
/**
* Index for the 5G-SA group index in the array returned by {@link
* #getInitialBitrateCountryGroupAssignment}.
*/
private static final int COUNTRY_GROUP_INDEX_5G_SA = 5;
/** Builder for a bandwidth meter. */
public static final class Builder {
private final Context context;
private Map<Integer, Long> initialBitrateEstimates;
private TimeToFirstByteEstimator timeToFirstByteEstimator;
private BandwidthEstimator bandwidthEstimator;
private boolean resetOnNetworkTypeChange;
/**
* Creates a builder with default parameters and without listener.
*
* @param context A context.
*/
public Builder(Context context) {
// Handling of null is for backward compatibility only.
this.context = context.getApplicationContext();
initialBitrateEstimates = getInitialBitrateEstimatesForCountry(Util.getCountryCode(context));
timeToFirstByteEstimator =
new PercentileTimeToFirstByteEstimator(
/* numberOfSamples= */ DEFAULT_TIME_TO_FIRST_BYTE_SAMPLES,
/* percentile= */ DEFAULT_TIME_TO_FIRST_BYTE_PERCENTILE);
bandwidthEstimator = new SplitParallelSampleBandwidthEstimator.Builder().build();
resetOnNetworkTypeChange = true;
}
/**
* Sets the initial bitrate estimate in bits per second that should be assumed when a bandwidth
* estimate is unavailable.
*
* @param initialBitrateEstimate The initial bitrate estimate in bits per second.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setInitialBitrateEstimate(long initialBitrateEstimate) {
for (Integer networkType : initialBitrateEstimates.keySet()) {
setInitialBitrateEstimate(networkType, initialBitrateEstimate);
}
return this;
}
/**
* Sets the initial bitrate estimate in bits per second that should be assumed when a bandwidth
* estimate is unavailable and the current network connection is of the specified type.
*
* @param networkType The {@link C.NetworkType} this initial estimate is for.
* @param initialBitrateEstimate The initial bitrate estimate in bits per second.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setInitialBitrateEstimate(
@C.NetworkType int networkType, long initialBitrateEstimate) {
initialBitrateEstimates.put(networkType, initialBitrateEstimate);
return this;
}
/**
* Sets the initial bitrate estimates to the default values of the specified country. The
* initial estimates are used when a bandwidth estimate is unavailable.
*
* @param countryCode The ISO 3166-1 alpha-2 country code of the country whose default bitrate
* estimates should be used.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setInitialBitrateEstimate(String countryCode) {
initialBitrateEstimates =
getInitialBitrateEstimatesForCountry(Ascii.toUpperCase(countryCode));
return this;
}
/**
* Sets the {@link TimeToFirstByteEstimator} to be used.
*
* <p>Default is {@link PercentileTimeToFirstByteEstimator} with a sliding window size of {@link
* #DEFAULT_TIME_TO_FIRST_BYTE_SAMPLES} that uses a percentile of {@link
* #DEFAULT_TIME_TO_FIRST_BYTE_PERCENTILE}.
*
* @param timeToFirstByteEstimator The {@link TimeToFirstByteEstimator} to be used.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setTimeToFirstByteEstimator(TimeToFirstByteEstimator timeToFirstByteEstimator) {
this.timeToFirstByteEstimator = timeToFirstByteEstimator;
return this;
}
/**
* Sets the {@link BandwidthEstimator} used. By default, this is set to a {@link
* SplitParallelSampleBandwidthEstimator} using a {@link
* SlidingWeightedAverageBandwidthStatistic}.
*/
@CanIgnoreReturnValue
public Builder setBandwidthEstimator(BandwidthEstimator bandwidthEstimator) {
this.bandwidthEstimator = bandwidthEstimator;
return this;
}
/**
* Sets whether to reset if the network type changes. The default value is {@code true}.
*
* @param resetOnNetworkTypeChange Whether to reset if the network type changes.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setResetOnNetworkTypeChange(boolean resetOnNetworkTypeChange) {
this.resetOnNetworkTypeChange = resetOnNetworkTypeChange;
return this;
}
/**
* Builds the bandwidth meter.
*
* @return A bandwidth meter with the configured properties.
*/
public ExperimentalBandwidthMeter build() {
return new ExperimentalBandwidthMeter(
context,
initialBitrateEstimates,
timeToFirstByteEstimator,
bandwidthEstimator,
resetOnNetworkTypeChange);
}
private static Map<Integer, Long> getInitialBitrateEstimatesForCountry(String countryCode) {
int[] groupIndices = getInitialBitrateCountryGroupAssignment(countryCode);
Map<Integer, Long> result = new HashMap<>(/* initialCapacity= */ 8);
result.put(C.NETWORK_TYPE_UNKNOWN, DEFAULT_INITIAL_BITRATE_ESTIMATE);
result.put(
C.NETWORK_TYPE_WIFI,
DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI.get(groupIndices[COUNTRY_GROUP_INDEX_WIFI]));
result.put(
C.NETWORK_TYPE_2G,
DEFAULT_INITIAL_BITRATE_ESTIMATES_2G.get(groupIndices[COUNTRY_GROUP_INDEX_2G]));
result.put(
C.NETWORK_TYPE_3G,
DEFAULT_INITIAL_BITRATE_ESTIMATES_3G.get(groupIndices[COUNTRY_GROUP_INDEX_3G]));
result.put(
C.NETWORK_TYPE_4G,
DEFAULT_INITIAL_BITRATE_ESTIMATES_4G.get(groupIndices[COUNTRY_GROUP_INDEX_4G]));
result.put(
C.NETWORK_TYPE_5G_NSA,
DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_NSA.get(groupIndices[COUNTRY_GROUP_INDEX_5G_NSA]));
result.put(
C.NETWORK_TYPE_5G_SA,
DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_SA.get(groupIndices[COUNTRY_GROUP_INDEX_5G_SA]));
// Assume default Wifi speed for Ethernet to prevent using the slower fallback.
result.put(
C.NETWORK_TYPE_ETHERNET,
DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI.get(groupIndices[COUNTRY_GROUP_INDEX_WIFI]));
return result;
}
}
private final ImmutableMap<Integer, Long> initialBitrateEstimates;
private final TimeToFirstByteEstimator timeToFirstByteEstimator;
private final BandwidthEstimator bandwidthEstimator;
private final boolean resetOnNetworkTypeChange;
private @C.NetworkType int networkType;
private long initialBitrateEstimate;
private boolean networkTypeOverrideSet;
private @C.NetworkType int networkTypeOverride;
private ExperimentalBandwidthMeter(
Context context,
Map<Integer, Long> initialBitrateEstimates,
TimeToFirstByteEstimator timeToFirstByteEstimator,
BandwidthEstimator bandwidthEstimator,
boolean resetOnNetworkTypeChange) {
this.initialBitrateEstimates = ImmutableMap.copyOf(initialBitrateEstimates);
this.timeToFirstByteEstimator = timeToFirstByteEstimator;
this.bandwidthEstimator = bandwidthEstimator;
this.resetOnNetworkTypeChange = resetOnNetworkTypeChange;
NetworkTypeObserver networkTypeObserver = NetworkTypeObserver.getInstance(context);
networkType = networkTypeObserver.getNetworkType();
initialBitrateEstimate = getInitialBitrateEstimateForNetworkType(networkType);
networkTypeObserver.register(/* listener= */ this::onNetworkTypeChanged);
}
/**
* Overrides the network type. Handled in the same way as if the meter had detected a change from
* the current network type to the specified network type internally.
*
* <p>Applications should not normally call this method. It is intended for testing purposes.
*
* @param networkType The overriding network type.
*/
public synchronized void setNetworkTypeOverride(@C.NetworkType int networkType) {
networkTypeOverride = networkType;
networkTypeOverrideSet = true;
onNetworkTypeChanged(networkType);
}
@Override
public synchronized long getBitrateEstimate() {
long bandwidthEstimate = bandwidthEstimator.getBandwidthEstimate();
return bandwidthEstimate != BandwidthEstimator.ESTIMATE_NOT_AVAILABLE
? bandwidthEstimate
: initialBitrateEstimate;
}
@Override
public long getTimeToFirstByteEstimateUs() {
return timeToFirstByteEstimator.getTimeToFirstByteEstimateUs();
}
@Override
public TransferListener getTransferListener() {
return this;
}
@Override
public void addEventListener(Handler eventHandler, EventListener eventListener) {
checkNotNull(eventHandler);
checkNotNull(eventListener);
bandwidthEstimator.addEventListener(eventHandler, eventListener);
}
@Override
public void removeEventListener(EventListener eventListener) {
bandwidthEstimator.removeEventListener(eventListener);
}
@Override
public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) {
if (!isTransferAtFullNetworkSpeed(dataSpec, isNetwork)) {
return;
}
timeToFirstByteEstimator.onTransferInitializing(dataSpec);
bandwidthEstimator.onTransferInitializing(source);
}
@Override
public synchronized void onTransferStart(
DataSource source, DataSpec dataSpec, boolean isNetwork) {
if (!isTransferAtFullNetworkSpeed(dataSpec, isNetwork)) {
return;
}
timeToFirstByteEstimator.onTransferStart(dataSpec);
bandwidthEstimator.onTransferStart(source);
}
@Override
public synchronized void onBytesTransferred(
DataSource source, DataSpec dataSpec, boolean isNetwork, int bytesTransferred) {
if (!isTransferAtFullNetworkSpeed(dataSpec, isNetwork)) {
return;
}
bandwidthEstimator.onBytesTransferred(source, bytesTransferred);
}
@Override
public synchronized void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) {
if (!isTransferAtFullNetworkSpeed(dataSpec, isNetwork)) {
return;
}
bandwidthEstimator.onTransferEnd(source);
}
private synchronized void onNetworkTypeChanged(@C.NetworkType int networkType) {
if (this.networkType != C.NETWORK_TYPE_UNKNOWN && !resetOnNetworkTypeChange) {
// Reset on network change disabled. Ignore all updates except the initial one.
return;
}
if (networkTypeOverrideSet) {
networkType = networkTypeOverride;
}
if (this.networkType == networkType) {
return;
}
this.networkType = networkType;
if (networkType == C.NETWORK_TYPE_OFFLINE
|| networkType == C.NETWORK_TYPE_UNKNOWN
|| networkType == C.NETWORK_TYPE_OTHER) {
// It's better not to reset the bandwidth meter for these network types.
return;
}
// Reset the bitrate estimate and report it, along with any bytes transferred.
this.initialBitrateEstimate = getInitialBitrateEstimateForNetworkType(networkType);
bandwidthEstimator.onNetworkTypeChange(initialBitrateEstimate);
timeToFirstByteEstimator.reset();
}
private long getInitialBitrateEstimateForNetworkType(@C.NetworkType int networkType) {
@Nullable Long initialBitrateEstimate = initialBitrateEstimates.get(networkType);
if (initialBitrateEstimate == null) {
initialBitrateEstimate = initialBitrateEstimates.get(C.NETWORK_TYPE_UNKNOWN);
}
if (initialBitrateEstimate == null) {
initialBitrateEstimate = DEFAULT_INITIAL_BITRATE_ESTIMATE;
}
return initialBitrateEstimate;
}
private static boolean isTransferAtFullNetworkSpeed(DataSpec dataSpec, boolean isNetwork) {
return isNetwork && !dataSpec.isFlagSet(DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED);
}
/**
* Returns initial bitrate group assignments for a {@code country}. The initial bitrate is a list
* of indices for [Wifi, 2G, 3G, 4G, 5G_NSA, 5G_SA].
*/
private static int[] getInitialBitrateCountryGroupAssignment(String country) {
switch (country) {
case "AD":
case "CW":
return new int[] {2, 2, 0, 0, 2, 2};
case "AE":
return new int[] {1, 4, 3, 4, 4, 2};
case "AG":
return new int[] {2, 4, 3, 4, 2, 2};
case "AL":
return new int[] {1, 1, 1, 3, 2, 2};
case "AM":
return new int[] {2, 3, 2, 3, 2, 2};
case "AO":
return new int[] {4, 4, 4, 3, 2, 2};
case "AS":
return new int[] {2, 2, 3, 3, 2, 2};
case "AT":
return new int[] {1, 2, 1, 4, 1, 4};
case "AU":
return new int[] {0, 2, 1, 1, 3, 0};
case "BE":
return new int[] {0, 1, 4, 4, 3, 2};
case "BH":
return new int[] {1, 3, 1, 4, 4, 2};
case "BJ":
return new int[] {4, 4, 2, 3, 2, 2};
case "BN":
return new int[] {3, 2, 0, 1, 2, 2};
case "BO":
return new int[] {1, 2, 3, 2, 2, 2};
case "BR":
return new int[] {1, 1, 2, 1, 1, 0};
case "BW":
return new int[] {3, 2, 1, 0, 2, 2};
case "BY":
return new int[] {1, 1, 2, 3, 2, 2};
case "CA":
return new int[] {0, 2, 3, 3, 3, 3};
case "CH":
return new int[] {0, 0, 0, 0, 0, 3};
case "BZ":
case "CK":
return new int[] {2, 2, 2, 1, 2, 2};
case "CL":
return new int[] {1, 1, 2, 1, 3, 2};
case "CM":
return new int[] {4, 3, 3, 4, 2, 2};
case "CN":
return new int[] {2, 0, 4, 3, 3, 1};
case "CO":
return new int[] {2, 3, 4, 2, 2, 2};
case "CR":
return new int[] {2, 4, 4, 4, 2, 2};
case "CV":
return new int[] {2, 3, 0, 1, 2, 2};
case "CZ":
return new int[] {0, 0, 2, 0, 1, 2};
case "DE":
return new int[] {0, 1, 3, 2, 2, 2};
case "DO":
return new int[] {3, 4, 4, 4, 4, 2};
case "AZ":
case "BF":
case "DZ":
return new int[] {3, 3, 4, 4, 2, 2};
case "EC":
return new int[] {1, 3, 2, 1, 2, 2};
case "CI":
case "EG":
return new int[] {3, 4, 3, 3, 2, 2};
case "FI":
return new int[] {0, 0, 0, 2, 0, 2};
case "FJ":
return new int[] {3, 1, 2, 3, 2, 2};
case "FM":
return new int[] {4, 2, 3, 0, 2, 2};
case "AI":
case "BB":
case "BM":
case "BQ":
case "DM":
case "FO":
return new int[] {0, 2, 0, 0, 2, 2};
case "FR":
return new int[] {1, 1, 2, 1, 1, 2};
case "GB":
return new int[] {0, 1, 1, 2, 1, 2};
case "GE":
return new int[] {1, 0, 0, 2, 2, 2};
case "GG":
return new int[] {0, 2, 1, 0, 2, 2};
case "CG":
case "GH":
return new int[] {3, 3, 3, 3, 2, 2};
case "GM":
return new int[] {4, 3, 2, 4, 2, 2};
case "GN":
return new int[] {4, 4, 4, 2, 2, 2};
case "GP":
return new int[] {3, 1, 1, 3, 2, 2};
case "GQ":
return new int[] {4, 4, 3, 3, 2, 2};
case "GT":
return new int[] {2, 2, 2, 1, 1, 2};
case "AW":
case "GU":
return new int[] {1, 2, 4, 4, 2, 2};
case "GW":
return new int[] {4, 4, 2, 2, 2, 2};
case "GY":
return new int[] {3, 0, 1, 1, 2, 2};
case "HK":
return new int[] {0, 1, 1, 3, 2, 0};
case "HN":
return new int[] {3, 3, 2, 2, 2, 2};
case "ID":
return new int[] {3, 1, 1, 2, 3, 2};
case "BA":
case "IE":
return new int[] {1, 1, 1, 1, 2, 2};
case "IL":
return new int[] {1, 2, 2, 3, 4, 2};
case "IM":
return new int[] {0, 2, 0, 1, 2, 2};
case "IN":
return new int[] {1, 1, 2, 1, 2, 1};
case "IR":
return new int[] {4, 2, 3, 3, 4, 2};
case "IS":
return new int[] {0, 0, 1, 0, 0, 2};
case "IT":
return new int[] {0, 0, 1, 1, 1, 2};
case "GI":
case "JE":
return new int[] {1, 2, 0, 1, 2, 2};
case "JM":
return new int[] {2, 4, 2, 1, 2, 2};
case "JO":
return new int[] {2, 0, 1, 1, 2, 2};
case "JP":
return new int[] {0, 3, 3, 3, 4, 4};
case "KE":
return new int[] {3, 2, 2, 1, 2, 2};
case "KH":
return new int[] {1, 0, 4, 2, 2, 2};
case "CU":
case "KI":
return new int[] {4, 2, 4, 3, 2, 2};
case "CD":
case "KM":
return new int[] {4, 3, 3, 2, 2, 2};
case "KR":
return new int[] {0, 2, 2, 4, 4, 4};
case "KW":
return new int[] {1, 0, 1, 0, 0, 2};
case "BD":
case "KZ":
return new int[] {2, 1, 2, 2, 2, 2};
case "LA":
return new int[] {1, 2, 1, 3, 2, 2};
case "BS":
case "LB":
return new int[] {3, 2, 1, 2, 2, 2};
case "LK":
return new int[] {3, 2, 3, 4, 4, 2};
case "LR":
return new int[] {3, 4, 3, 4, 2, 2};
case "LU":
return new int[] {1, 1, 4, 2, 0, 2};
case "CY":
case "HR":
case "LV":
return new int[] {1, 0, 0, 0, 0, 2};
case "MA":
return new int[] {3, 3, 2, 1, 2, 2};
case "MC":
return new int[] {0, 2, 2, 0, 2, 2};
case "MD":
return new int[] {1, 0, 0, 0, 2, 2};
case "ME":
return new int[] {2, 0, 0, 1, 1, 2};
case "MH":
return new int[] {4, 2, 1, 3, 2, 2};
case "MK":
return new int[] {2, 0, 0, 1, 3, 2};
case "MM":
return new int[] {2, 2, 2, 3, 4, 2};
case "MN":
return new int[] {2, 0, 1, 2, 2, 2};
case "MO":
return new int[] {0, 2, 4, 4, 4, 2};
case "KG":
case "MQ":
return new int[] {2, 1, 1, 2, 2, 2};
case "MR":
return new int[] {4, 2, 3, 4, 2, 2};
case "DK":
case "EE":
case "HU":
case "LT":
case "MT":
return new int[] {0, 0, 0, 0, 0, 2};
case "MV":
return new int[] {3, 4, 1, 3, 3, 2};
case "MW":
return new int[] {4, 2, 3, 3, 2, 2};
case "MX":
return new int[] {3, 4, 4, 4, 2, 2};
case "MY":
return new int[] {1, 0, 4, 1, 2, 2};
case "NA":
return new int[] {3, 4, 3, 2, 2, 2};
case "NC":
return new int[] {3, 2, 3, 4, 2, 2};
case "NG":
return new int[] {3, 4, 2, 1, 2, 2};
case "NI":
return new int[] {2, 3, 4, 3, 2, 2};
case "NL":
return new int[] {0, 2, 3, 3, 0, 4};
case "NO":
return new int[] {0, 1, 2, 1, 1, 2};
case "NP":
return new int[] {2, 1, 4, 3, 2, 2};
case "NR":
return new int[] {4, 0, 3, 2, 2, 2};
case "NU":
return new int[] {4, 2, 2, 1, 2, 2};
case "NZ":
return new int[] {1, 0, 2, 2, 4, 2};
case "OM":
return new int[] {2, 3, 1, 3, 4, 2};
case "PA":
return new int[] {2, 3, 3, 3, 2, 2};
case "PE":
return new int[] {1, 2, 4, 4, 3, 2};
case "AF":
case "PG":
return new int[] {4, 3, 3, 3, 2, 2};
case "PH":
return new int[] {2, 1, 3, 2, 2, 0};
case "PL":
return new int[] {2, 1, 2, 2, 4, 2};
case "PR":
return new int[] {2, 0, 2, 0, 2, 1};
case "PS":
return new int[] {3, 4, 1, 4, 2, 2};
case "PT":
return new int[] {1, 0, 0, 0, 1, 2};
case "PW":
return new int[] {2, 2, 4, 2, 2, 2};
case "BL":
case "MF":
case "PY":
return new int[] {1, 2, 2, 2, 2, 2};
case "QA":
return new int[] {1, 4, 4, 4, 4, 2};
case "RE":
return new int[] {1, 2, 2, 3, 1, 2};
case "RO":
return new int[] {0, 0, 1, 2, 1, 2};
case "RS":
return new int[] {2, 0, 0, 0, 2, 2};
case "RU":
return new int[] {1, 0, 0, 0, 3, 3};
case "RW":
return new int[] {3, 3, 1, 0, 2, 2};
case "MU":
case "SA":
return new int[] {3, 1, 1, 2, 2, 2};
case "CF":
case "SB":
return new int[] {4, 2, 4, 2, 2, 2};
case "SC":
return new int[] {4, 3, 1, 1, 2, 2};
case "SD":
return new int[] {4, 3, 4, 2, 2, 2};
case "SE":
return new int[] {0, 1, 1, 1, 0, 2};
case "SG":
return new int[] {2, 3, 3, 3, 3, 3};
case "AQ":
case "ER":
case "SH":
return new int[] {4, 2, 2, 2, 2, 2};
case "BG":
case "ES":
case "GR":
case "SI":
return new int[] {0, 0, 0, 0, 1, 2};
case "IQ":
case "SJ":
return new int[] {3, 2, 2, 2, 2, 2};
case "SK":
return new int[] {1, 1, 1, 1, 3, 2};
case "GF":
case "PK":
case "SL":
return new int[] {3, 2, 3, 3, 2, 2};
case "ET":
case "SN":
return new int[] {4, 4, 3, 2, 2, 2};
case "SO":
return new int[] {3, 2, 2, 4, 4, 2};
case "SR":
return new int[] {2, 4, 3, 0, 2, 2};
case "ST":
return new int[] {2, 2, 1, 2, 2, 2};
case "PF":
case "SV":
return new int[] {2, 3, 3, 1, 2, 2};
case "SZ":
return new int[] {4, 4, 3, 4, 2, 2};
case "TC":
return new int[] {2, 2, 1, 3, 2, 2};
case "GA":
case "TG":
return new int[] {3, 4, 1, 0, 2, 2};
case "TH":
return new int[] {0, 1, 2, 1, 2, 2};
case "DJ":
case "SY":
case "TJ":
return new int[] {4, 3, 4, 4, 2, 2};
case "GL":
case "TK":
return new int[] {2, 2, 2, 4, 2, 2};
case "TL":
return new int[] {4, 2, 4, 4, 2, 2};
case "SS":
case "TM":
return new int[] {4, 2, 2, 3, 2, 2};
case "TR":
return new int[] {1, 0, 0, 1, 3, 2};
case "TT":
return new int[] {1, 4, 0, 0, 2, 2};
case "TW":
return new int[] {0, 2, 0, 0, 0, 0};
case "ML":
case "TZ":
return new int[] {3, 4, 2, 2, 2, 2};
case "UA":
return new int[] {0, 1, 1, 2, 4, 2};
case "LS":
case "UG":
return new int[] {3, 3, 3, 2, 2, 2};
case "US":
return new int[] {1, 1, 4, 1, 3, 1};
case "TN":
case "UY":
return new int[] {2, 1, 1, 1, 2, 2};
case "UZ":
return new int[] {2, 2, 3, 4, 3, 2};
case "AX":
case "CX":
case "LI":
case "MP":
case "MS":
case "PM":
case "SM":
case "VA":
return new int[] {0, 2, 2, 2, 2, 2};
case "GD":
case "KN":
case "KY":
case "LC":
case "SX":
case "VC":
return new int[] {1, 2, 0, 0, 2, 2};
case "VG":
return new int[] {2, 2, 0, 1, 2, 2};
case "VI":
return new int[] {0, 2, 1, 2, 2, 2};
case "VN":
return new int[] {0, 0, 1, 2, 2, 1};
case "VU":
return new int[] {4, 3, 3, 1, 2, 2};
case "IO":
case "TV":
case "WF":
return new int[] {4, 2, 2, 4, 2, 2};
case "BT":
case "MZ":
case "WS":
return new int[] {3, 1, 2, 1, 2, 2};
case "XK":
return new int[] {1, 2, 1, 1, 2, 2};
case "BI":
case "HT":
case "MG":
case "NE":
case "TD":
case "VE":
case "YE":
return new int[] {4, 4, 4, 4, 2, 2};
case "YT":
return new int[] {2, 3, 3, 4, 2, 2};
case "ZA":
return new int[] {2, 3, 2, 1, 2, 2};
case "ZM":
return new int[] {4, 4, 4, 3, 3, 2};
case "LY":
case "TO":
case "ZW":
return new int[] {3, 2, 4, 3, 2, 2};
default:
return new int[] {2, 2, 2, 2, 2, 2};
}
}
}

View file

@ -0,0 +1,69 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import static androidx.media3.exoplayer.upstream.experimental.BandwidthEstimator.ESTIMATE_NOT_AVAILABLE;
import androidx.media3.common.util.UnstableApi;
/** A {@link BandwidthStatistic} that calculates estimates using an exponential weighted average. */
@UnstableApi
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;
}
}

View file

@ -0,0 +1,120 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.upstream.TimeToFirstByteEstimator;
import java.util.LinkedHashMap;
import java.util.Map;
/** Implementation of {@link TimeToFirstByteEstimator} based on exponential weighted average. */
@UnstableApi
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;
}
}
}

View file

@ -0,0 +1,133 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import static androidx.media3.common.util.Assertions.checkArgument;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.upstream.SlidingPercentile;
import androidx.media3.exoplayer.upstream.TimeToFirstByteEstimator;
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.
*/
@UnstableApi
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;
}
}
}

View file

@ -0,0 +1,144 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.exoplayer.upstream.experimental.BandwidthEstimator.ESTIMATE_NOT_AVAILABLE;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.util.ArrayDeque;
import java.util.TreeSet;
/**
* A {@link BandwidthStatistic} that calculates estimates based on a sliding window weighted
* percentile.
*/
@UnstableApi
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);
}
}
}

View file

@ -0,0 +1,158 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import static androidx.media3.common.util.Util.castNonNull;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.UnstableApi;
import java.util.ArrayDeque;
import java.util.Deque;
/**
* A {@link BandwidthStatistic} that calculates estimates based on a sliding window weighted
* average.
*/
@UnstableApi
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;
}
}

View file

@ -0,0 +1,210 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import android.os.Handler;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
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.
*/
@UnstableApi
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);
}
}

View file

@ -0,0 +1,19 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import androidx.media3.common.util.NonNullApi;

View file

@ -0,0 +1,209 @@
/*
* 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 androidx.media3.exoplayer.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.media3.datasource.DataSource;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
import androidx.media3.test.utils.FakeClock;
import androidx.media3.test.utils.FakeDataSource;
import androidx.test.ext.junit.runners.AndroidJUnit4;
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);
}
}

View file

@ -0,0 +1,734 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import static android.net.NetworkInfo.State.CONNECTED;
import static android.net.NetworkInfo.State.DISCONNECTED;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState;
import android.net.Uri;
import android.telephony.TelephonyDisplayInfo;
import android.telephony.TelephonyManager;
import androidx.media3.common.C;
import androidx.media3.common.util.NetworkTypeObserver;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec;
import androidx.media3.test.utils.FakeDataSource;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.time.Duration;
import java.util.Random;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowNetworkInfo;
import org.robolectric.shadows.ShadowSystemClock;
import org.robolectric.shadows.ShadowTelephonyManager;
/** Unit test for {@link ExperimentalBandwidthMeter}. */
@RunWith(AndroidJUnit4.class)
@Config(sdk = Config.ALL_SDKS) // Test all SDKs because network detection logic changed over time.
public final class ExperimentalBandwidthMeterTest {
private static final int SIMULATED_TRANSFER_COUNT = 100;
private static final String FAST_COUNTRY_ISO = "TW";
private static final String SLOW_COUNTRY_ISO = "PG";
private TelephonyManager telephonyManager;
private ConnectivityManager connectivityManager;
private NetworkInfo networkInfoOffline;
private NetworkInfo networkInfoWifi;
private NetworkInfo networkInfo2g;
private NetworkInfo networkInfo3g;
private NetworkInfo networkInfo4g;
private NetworkInfo networkInfo5gSa;
private NetworkInfo networkInfoEthernet;
@Before
public void setUp() {
NetworkTypeObserver.resetForTests();
connectivityManager =
(ConnectivityManager)
ApplicationProvider.getApplicationContext()
.getSystemService(Context.CONNECTIVITY_SERVICE);
telephonyManager =
(TelephonyManager)
ApplicationProvider.getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
Shadows.shadowOf(telephonyManager).setNetworkCountryIso(FAST_COUNTRY_ISO);
networkInfoOffline =
ShadowNetworkInfo.newInstance(
DetailedState.DISCONNECTED,
ConnectivityManager.TYPE_WIFI,
/* subType= */ 0,
/* isAvailable= */ false,
DISCONNECTED);
networkInfoWifi =
ShadowNetworkInfo.newInstance(
DetailedState.CONNECTED,
ConnectivityManager.TYPE_WIFI,
/* subType= */ 0,
/* isAvailable= */ true,
CONNECTED);
networkInfo2g =
ShadowNetworkInfo.newInstance(
DetailedState.CONNECTED,
ConnectivityManager.TYPE_MOBILE,
TelephonyManager.NETWORK_TYPE_GPRS,
/* isAvailable= */ true,
CONNECTED);
networkInfo3g =
ShadowNetworkInfo.newInstance(
DetailedState.CONNECTED,
ConnectivityManager.TYPE_MOBILE,
TelephonyManager.NETWORK_TYPE_HSDPA,
/* isAvailable= */ true,
CONNECTED);
networkInfo4g =
ShadowNetworkInfo.newInstance(
DetailedState.CONNECTED,
ConnectivityManager.TYPE_MOBILE,
TelephonyManager.NETWORK_TYPE_LTE,
/* isAvailable= */ true,
CONNECTED);
networkInfo5gSa =
ShadowNetworkInfo.newInstance(
DetailedState.CONNECTED,
ConnectivityManager.TYPE_MOBILE,
TelephonyManager.NETWORK_TYPE_NR,
/* isAvailable= */ true,
CONNECTED);
networkInfoEthernet =
ShadowNetworkInfo.newInstance(
DetailedState.CONNECTED,
ConnectivityManager.TYPE_ETHERNET,
/* subType= */ 0,
/* isAvailable= */ true,
CONNECTED);
setNetworkCountryIso("non-existent-country-to-force-default-values");
}
@Test
public void defaultInitialBitrateEstimate_forWifi_isGreaterThanEstimateFor2G() {
setActiveNetworkInfo(networkInfoWifi);
ExperimentalBandwidthMeter bandwidthMeterWifi =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateWifi = bandwidthMeterWifi.getBitrateEstimate();
setActiveNetworkInfo(networkInfo2g);
ExperimentalBandwidthMeter bandwidthMeter2g =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate2g = bandwidthMeter2g.getBitrateEstimate();
assertThat(initialEstimateWifi).isGreaterThan(initialEstimate2g);
}
@Test
public void defaultInitialBitrateEstimate_forWifi_isGreaterThanEstimateFor3G() {
setActiveNetworkInfo(networkInfoWifi);
ExperimentalBandwidthMeter bandwidthMeterWifi =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateWifi = bandwidthMeterWifi.getBitrateEstimate();
setActiveNetworkInfo(networkInfo3g);
ExperimentalBandwidthMeter bandwidthMeter3g =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate3g = bandwidthMeter3g.getBitrateEstimate();
assertThat(initialEstimateWifi).isGreaterThan(initialEstimate3g);
}
@Test
public void defaultInitialBitrateEstimate_forEthernet_isGreaterThanEstimateFor2G() {
setActiveNetworkInfo(networkInfoEthernet);
ExperimentalBandwidthMeter bandwidthMeterEthernet =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateEthernet = bandwidthMeterEthernet.getBitrateEstimate();
setActiveNetworkInfo(networkInfo2g);
ExperimentalBandwidthMeter bandwidthMeter2g =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate2g = bandwidthMeter2g.getBitrateEstimate();
assertThat(initialEstimateEthernet).isGreaterThan(initialEstimate2g);
}
@Test
public void defaultInitialBitrateEstimate_forEthernet_isGreaterThanEstimateFor3G() {
setActiveNetworkInfo(networkInfoEthernet);
ExperimentalBandwidthMeter bandwidthMeterEthernet =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateEthernet = bandwidthMeterEthernet.getBitrateEstimate();
setActiveNetworkInfo(networkInfo3g);
ExperimentalBandwidthMeter bandwidthMeter3g =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate3g = bandwidthMeter3g.getBitrateEstimate();
assertThat(initialEstimateEthernet).isGreaterThan(initialEstimate3g);
}
@Test
public void defaultInitialBitrateEstimate_for4G_isGreaterThanEstimateFor2G() {
setActiveNetworkInfo(networkInfo4g);
ExperimentalBandwidthMeter bandwidthMeter4g =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate4g = bandwidthMeter4g.getBitrateEstimate();
setActiveNetworkInfo(networkInfo2g);
ExperimentalBandwidthMeter bandwidthMeter2g =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate2g = bandwidthMeter2g.getBitrateEstimate();
assertThat(initialEstimate4g).isGreaterThan(initialEstimate2g);
}
@Test
public void defaultInitialBitrateEstimate_for4G_isGreaterThanEstimateFor3G() {
setActiveNetworkInfo(networkInfo4g);
ExperimentalBandwidthMeter bandwidthMeter4g =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate4g = bandwidthMeter4g.getBitrateEstimate();
setActiveNetworkInfo(networkInfo3g);
ExperimentalBandwidthMeter bandwidthMeter3g =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate3g = bandwidthMeter3g.getBitrateEstimate();
assertThat(initialEstimate4g).isGreaterThan(initialEstimate3g);
}
@Test
public void defaultInitialBitrateEstimate_for3G_isGreaterThanEstimateFor2G() {
setActiveNetworkInfo(networkInfo3g);
ExperimentalBandwidthMeter bandwidthMeter3g =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate3g = bandwidthMeter3g.getBitrateEstimate();
setActiveNetworkInfo(networkInfo2g);
ExperimentalBandwidthMeter bandwidthMeter2g =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate2g = bandwidthMeter2g.getBitrateEstimate();
assertThat(initialEstimate3g).isGreaterThan(initialEstimate2g);
}
@Test
@Config(minSdk = 31) // 5G-NSA detection is supported from API 31.
public void defaultInitialBitrateEstimate_for5gNsa_isGreaterThanEstimateFor4g() {
setActiveNetworkInfo(networkInfo4g);
ExperimentalBandwidthMeter bandwidthMeter4g =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate4g = bandwidthMeter4g.getBitrateEstimate();
setActiveNetworkInfo(networkInfo4g, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA);
ExperimentalBandwidthMeter bandwidthMeter5gNsa =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate5gNsa = bandwidthMeter5gNsa.getBitrateEstimate();
assertThat(initialEstimate5gNsa).isGreaterThan(initialEstimate4g);
}
@Test
@Config(minSdk = 29) // 5G-SA detection is supported from API 29.
public void defaultInitialBitrateEstimate_for5gSa_isGreaterThanEstimateFor3g() {
setActiveNetworkInfo(networkInfo3g);
ExperimentalBandwidthMeter bandwidthMeter3g =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate3g = bandwidthMeter3g.getBitrateEstimate();
setActiveNetworkInfo(networkInfo5gSa);
ExperimentalBandwidthMeter bandwidthMeter5gSa =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate5gSa = bandwidthMeter5gSa.getBitrateEstimate();
assertThat(initialEstimate5gSa).isGreaterThan(initialEstimate3g);
}
@Test
public void defaultInitialBitrateEstimate_forOffline_isReasonable() {
setActiveNetworkInfo(networkInfoOffline);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isGreaterThan(100_000L);
assertThat(initialEstimate).isLessThan(50_000_000L);
}
@Test
public void
defaultInitialBitrateEstimate_forWifi_forFastCountry_isGreaterThanEstimateForSlowCountry() {
setActiveNetworkInfo(networkInfoWifi);
setNetworkCountryIso(FAST_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterFast =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate();
setNetworkCountryIso(SLOW_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterSlow =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();
assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow);
}
@Test
public void
defaultInitialBitrateEstimate_forEthernet_forFastCountry_isGreaterThanEstimateForSlowCountry() {
setActiveNetworkInfo(networkInfoEthernet);
setNetworkCountryIso(FAST_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterFast =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate();
setNetworkCountryIso(SLOW_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterSlow =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();
assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow);
}
@Test
public void
defaultInitialBitrateEstimate_for2G_forFastCountry_isGreaterThanEstimateForSlowCountry() {
setActiveNetworkInfo(networkInfo2g);
setNetworkCountryIso(FAST_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterFast =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate();
setNetworkCountryIso(SLOW_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterSlow =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();
assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow);
}
@Test
public void
defaultInitialBitrateEstimate_for3G_forFastCountry_isGreaterThanEstimateForSlowCountry() {
setActiveNetworkInfo(networkInfo3g);
setNetworkCountryIso(FAST_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterFast =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate();
setNetworkCountryIso(SLOW_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterSlow =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();
assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow);
}
@Test
public void
defaultInitialBitrateEstimate_for4g_forFastCountry_isGreaterThanEstimateForSlowCountry() {
setActiveNetworkInfo(networkInfo4g);
setNetworkCountryIso(FAST_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterFast =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate();
setNetworkCountryIso(SLOW_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterSlow =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();
assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow);
}
@Test
@Config(minSdk = 31) // 5G-NSA detection is supported from API 31.
public void
defaultInitialBitrateEstimate_for5gNsa_forFastCountry_isGreaterThanEstimateForSlowCountry() {
setActiveNetworkInfo(networkInfo4g, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA);
setNetworkCountryIso(FAST_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterFast =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate();
setNetworkCountryIso(SLOW_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterSlow =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();
assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow);
}
@Ignore // 5G-SA isn't widespread enough yet to define a slow and fast country for testing.
@Test
@Config(minSdk = 29) // 5G-SA detection is supported from API 29.
public void
defaultInitialBitrateEstimate_for5gSa_forFastCountry_isGreaterThanEstimateForSlowCountry() {
setActiveNetworkInfo(networkInfo5gSa);
setNetworkCountryIso(FAST_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterFast =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate();
setNetworkCountryIso(SLOW_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterSlow =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();
assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow);
}
@Test
public void initialBitrateEstimateOverwrite_whileConnectedToNetwork_setsInitialEstimate() {
setActiveNetworkInfo(networkInfoWifi);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isEqualTo(123456789);
}
@Test
public void initialBitrateEstimateOverwrite_whileOffline_setsInitialEstimate() {
setActiveNetworkInfo(networkInfoOffline);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isEqualTo(123456789);
}
@Test
public void initialBitrateEstimateOverwrite_forWifi_whileConnectedToWifi_setsInitialEstimate() {
setActiveNetworkInfo(networkInfoWifi);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_WIFI, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isEqualTo(123456789);
}
@Test
public void
initialBitrateEstimateOverwrite_forWifi_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() {
setActiveNetworkInfo(networkInfo2g);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_WIFI, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isNotEqualTo(123456789);
}
@Test
public void
initialBitrateEstimateOverwrite_forEthernet_whileConnectedToEthernet_setsInitialEstimate() {
setActiveNetworkInfo(networkInfoEthernet);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_ETHERNET, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isEqualTo(123456789);
}
@Test
public void
initialBitrateEstimateOverwrite_forEthernet_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() {
setActiveNetworkInfo(networkInfo2g);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_WIFI, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isNotEqualTo(123456789);
}
@Test
public void initialBitrateEstimateOverwrite_for2G_whileConnectedTo2G_setsInitialEstimate() {
setActiveNetworkInfo(networkInfo2g);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_2G, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isEqualTo(123456789);
}
@Test
public void
initialBitrateEstimateOverwrite_for2G_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() {
setActiveNetworkInfo(networkInfoWifi);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_2G, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isNotEqualTo(123456789);
}
@Test
public void initialBitrateEstimateOverwrite_for3G_whileConnectedTo3G_setsInitialEstimate() {
setActiveNetworkInfo(networkInfo3g);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_3G, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isEqualTo(123456789);
}
@Test
public void
initialBitrateEstimateOverwrite_for3G_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() {
setActiveNetworkInfo(networkInfoWifi);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_3G, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isNotEqualTo(123456789);
}
@Test
public void initialBitrateEstimateOverwrite_for4G_whileConnectedTo4G_setsInitialEstimate() {
setActiveNetworkInfo(networkInfo4g);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_4G, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isEqualTo(123456789);
}
@Test
public void
initialBitrateEstimateOverwrite_for4G_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() {
setActiveNetworkInfo(networkInfoWifi);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_4G, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isNotEqualTo(123456789);
}
@Test
@Config(minSdk = 31) // 5G-NSA detection is supported from API 31.
public void initialBitrateEstimateOverwrite_for5gNsa_whileConnectedTo5gNsa_setsInitialEstimate() {
setActiveNetworkInfo(networkInfo4g, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_5G_NSA, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isEqualTo(123456789);
}
@Test
@Config(minSdk = 31) // 5G-NSA detection is supported from API 31.
public void
initialBitrateEstimateOverwrite_for5gNsa_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() {
setActiveNetworkInfo(networkInfo4g);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_5G_NSA, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isNotEqualTo(123456789);
}
@Test
@Config(minSdk = 29) // 5G-SA detection is supported from API 29.
public void initialBitrateEstimateOverwrite_for5gSa_whileConnectedTo5gSa_setsInitialEstimate() {
setActiveNetworkInfo(networkInfo5gSa);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_5G_SA, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isEqualTo(123456789);
}
@Test
@Config(minSdk = 29) // 5G-SA detection is supported from API 29.
public void
initialBitrateEstimateOverwrite_for5gSa_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() {
setActiveNetworkInfo(networkInfoWifi);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_5G_SA, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isNotEqualTo(123456789);
}
@Test
public void initialBitrateEstimateOverwrite_forOffline_whileOffline_setsInitialEstimate() {
setActiveNetworkInfo(networkInfoOffline);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_OFFLINE, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isEqualTo(123456789);
}
@Test
public void
initialBitrateEstimateOverwrite_forOffline_whileConnectedToNetwork_doesNotSetInitialEstimate() {
setActiveNetworkInfo(networkInfoWifi);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(C.NETWORK_TYPE_OFFLINE, 123456789)
.build();
long initialEstimate = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimate).isNotEqualTo(123456789);
}
@Test
public void initialBitrateEstimateOverwrite_forCountry_usesDefaultValuesForCountry() {
setNetworkCountryIso(SLOW_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterSlow =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();
setNetworkCountryIso(FAST_COUNTRY_ISO);
ExperimentalBandwidthMeter bandwidthMeterFastWithSlowOverwrite =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())
.setInitialBitrateEstimate(SLOW_COUNTRY_ISO)
.build();
long initialEstimateFastWithSlowOverwrite =
bandwidthMeterFastWithSlowOverwrite.getBitrateEstimate();
assertThat(initialEstimateFastWithSlowOverwrite).isEqualTo(initialEstimateSlow);
}
@Test
public void networkTypeOverride_updatesBitrateEstimate() {
setActiveNetworkInfo(networkInfoEthernet);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long initialEstimateEthernet = bandwidthMeter.getBitrateEstimate();
bandwidthMeter.setNetworkTypeOverride(C.NETWORK_TYPE_2G);
long initialEstimate2g = bandwidthMeter.getBitrateEstimate();
assertThat(initialEstimateEthernet).isGreaterThan(initialEstimate2g);
}
@Test
public void networkTypeOverride_doesFullReset() {
// Simulate transfers for an ethernet connection.
setActiveNetworkInfo(networkInfoEthernet);
ExperimentalBandwidthMeter bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
long[] bitrateEstimatesWithNewInstance = simulateTransfers(bandwidthMeter);
// Create a new instance and seed with some transfers.
setActiveNetworkInfo(networkInfo2g);
bandwidthMeter =
new ExperimentalBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
simulateTransfers(bandwidthMeter);
// Override the network type to ethernet and simulate transfers again.
bandwidthMeter.setNetworkTypeOverride(C.NETWORK_TYPE_ETHERNET);
long[] bitrateEstimatesAfterReset = simulateTransfers(bandwidthMeter);
// If overriding the network type fully reset the bandwidth meter, we expect the bitrate
// estimates generated during simulation to be the same.
assertThat(bitrateEstimatesAfterReset).isEqualTo(bitrateEstimatesWithNewInstance);
}
private void setActiveNetworkInfo(NetworkInfo networkInfo) {
setActiveNetworkInfo(networkInfo, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE);
}
@SuppressWarnings("StickyBroadcast")
private void setActiveNetworkInfo(NetworkInfo networkInfo, int networkTypeOverride) {
// Set network info in ConnectivityManager and TelephonyDisplayInfo in TelephonyManager.
Shadows.shadowOf(connectivityManager).setActiveNetworkInfo(networkInfo);
if (Util.SDK_INT >= 31) {
Object displayInfo =
ShadowTelephonyManager.createTelephonyDisplayInfo(
networkInfo.getType(), networkTypeOverride);
Shadows.shadowOf(telephonyManager).setTelephonyDisplayInfo(displayInfo);
}
// Create a sticky broadcast for the connectivity action because Robolectric isn't replying with
// the current network state if a receiver for this intent is registered.
ApplicationProvider.getApplicationContext()
.sendStickyBroadcast(new Intent(ConnectivityManager.CONNECTIVITY_ACTION));
// Trigger initialization of static network type observer.
NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext());
// Wait until all pending messages are handled and the network initialization is done.
ShadowLooper.idleMainLooper();
}
private void setNetworkCountryIso(String countryIso) {
Shadows.shadowOf(telephonyManager).setNetworkCountryIso(countryIso);
}
private static long[] simulateTransfers(ExperimentalBandwidthMeter bandwidthMeter) {
long[] bitrateEstimates = new long[SIMULATED_TRANSFER_COUNT];
Random random = new Random(/* seed= */ 0);
DataSource dataSource = new FakeDataSource();
DataSpec dataSpec = new DataSpec(Uri.parse("https://test.com"));
for (int i = 0; i < SIMULATED_TRANSFER_COUNT; i++) {
bandwidthMeter.onTransferInitializing(dataSource, dataSpec, /* isNetwork= */ true);
ShadowSystemClock.advanceBy(Duration.ofMillis(random.nextInt(50)));
bandwidthMeter.onTransferStart(dataSource, dataSpec, /* isNetwork= */ true);
ShadowSystemClock.advanceBy(Duration.ofMillis(random.nextInt(5000)));
bandwidthMeter.onBytesTransferred(
dataSource,
dataSpec,
/* isNetwork= */ true,
/* bytesTransferred= */ random.nextInt(5 * 1024 * 1024));
bandwidthMeter.onTransferEnd(dataSource, dataSpec, /* isNetwork= */ true);
bitrateEstimates[i] = bandwidthMeter.getBitrateEstimate();
}
return bitrateEstimates;
}
}

View file

@ -0,0 +1,83 @@
/*
* 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 androidx.media3.exoplayer.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);
}
}

View file

@ -0,0 +1,101 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import static androidx.media3.exoplayer.upstream.experimental.ExponentialWeightedAverageTimeToFirstByteEstimator.DEFAULT_SMOOTHING_FACTOR;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.media3.common.C;
import androidx.media3.datasource.DataSpec;
import androidx.media3.test.utils.FakeClock;
import androidx.test.ext.junit.runners.AndroidJUnit4;
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);
}
}

View file

@ -0,0 +1,167 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.Uri;
import androidx.media3.common.C;
import androidx.media3.datasource.DataSpec;
import androidx.test.ext.junit.runners.AndroidJUnit4;
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);
}
}

View file

@ -0,0 +1,120 @@
/*
* 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 androidx.media3.exoplayer.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);
}
}

View file

@ -0,0 +1,169 @@
/*
* 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 androidx.media3.exoplayer.upstream.experimental;
import static com.google.common.truth.Truth.assertThat;
import androidx.media3.test.utils.FakeClock;
import androidx.test.ext.junit.runners.AndroidJUnit4;
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);
}
}

View file

@ -0,0 +1,202 @@
/*
* 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 androidx.media3.exoplayer.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.media3.datasource.DataSource;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
import androidx.media3.test.utils.FakeClock;
import androidx.media3.test.utils.FakeDataSource;
import androidx.test.ext.junit.runners.AndroidJUnit4;
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);
}
}