diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/BandwidthEstimator.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/BandwidthEstimator.java
new file mode 100644
index 0000000000..e1136a878e
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/BandwidthEstimator.java
@@ -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);
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/BandwidthStatistic.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/BandwidthStatistic.java
new file mode 100644
index 0000000000..afd9386008
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/BandwidthStatistic.java
@@ -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();
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/CombinedParallelSampleBandwidthEstimator.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/CombinedParallelSampleBandwidthEstimator.java
new file mode 100644
index 0000000000..7ce30f19e3
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/CombinedParallelSampleBandwidthEstimator.java
@@ -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);
+ }
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/ExperimentalBandwidthMeter.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/ExperimentalBandwidthMeter.java
new file mode 100644
index 0000000000..9f00daf019
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/ExperimentalBandwidthMeter.java
@@ -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.
+ *
+ *
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 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 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 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 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 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 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 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.
+ *
+ * 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 getInitialBitrateEstimatesForCountry(String countryCode) {
+ int[] groupIndices = getInitialBitrateCountryGroupAssignment(countryCode);
+ Map 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 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 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.
+ *
+ * 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};
+ }
+ }
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/ExponentialWeightedAverageStatistic.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/ExponentialWeightedAverageStatistic.java
new file mode 100644
index 0000000000..c21246dcf0
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/ExponentialWeightedAverageStatistic.java
@@ -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;
+ }
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/ExponentialWeightedAverageTimeToFirstByteEstimator.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/ExponentialWeightedAverageTimeToFirstByteEstimator.java
new file mode 100644
index 0000000000..783f556800
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/ExponentialWeightedAverageTimeToFirstByteEstimator.java
@@ -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 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 extends LinkedHashMap {
+
+ private final int maxSize;
+
+ public FixedSizeLinkedHashMap(int maxSize) {
+ this.maxSize = maxSize;
+ }
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > maxSize;
+ }
+ }
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/PercentileTimeToFirstByteEstimator.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/PercentileTimeToFirstByteEstimator.java
new file mode 100644
index 0000000000..dbeb3d3db0
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/PercentileTimeToFirstByteEstimator.java
@@ -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 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 extends LinkedHashMap {
+
+ private final int maxSize;
+
+ public FixedSizeLinkedHashMap(int maxSize) {
+ this.maxSize = maxSize;
+ }
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > maxSize;
+ }
+ }
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/SlidingPercentileBandwidthStatistic.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/SlidingPercentileBandwidthStatistic.java
new file mode 100644
index 0000000000..ff6d73d303
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/SlidingPercentileBandwidthStatistic.java
@@ -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 samples;
+ private final TreeSet 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 {
+ 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);
+ }
+ }
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/SlidingWeightedAverageBandwidthStatistic.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/SlidingWeightedAverageBandwidthStatistic.java
new file mode 100644
index 0000000000..e4de6cd337
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/SlidingWeightedAverageBandwidthStatistic.java
@@ -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 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 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;
+ }
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/SplitParallelSampleBandwidthEstimator.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/SplitParallelSampleBandwidthEstimator.java
new file mode 100644
index 0000000000..a3b14c8fbb
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/SplitParallelSampleBandwidthEstimator.java
@@ -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);
+ }
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/package-info.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/package-info.java
new file mode 100644
index 0000000000..cd49b5430a
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/experimental/package-info.java
@@ -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;
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/CombinedParallelSampleBandwidthEstimatorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/CombinedParallelSampleBandwidthEstimatorTest.java
new file mode 100644
index 0000000000..b819961b95
--- /dev/null
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/CombinedParallelSampleBandwidthEstimatorTest.java
@@ -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);
+ }
+}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExperimentalBandwidthMeterTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExperimentalBandwidthMeterTest.java
new file mode 100644
index 0000000000..292a52f25f
--- /dev/null
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExperimentalBandwidthMeterTest.java
@@ -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;
+ }
+}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExponentialWeightedAverageStatisticTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExponentialWeightedAverageStatisticTest.java
new file mode 100644
index 0000000000..b89299c9c3
--- /dev/null
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExponentialWeightedAverageStatisticTest.java
@@ -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);
+ }
+}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExponentialWeightedAverageTimeToFirstByteEstimatorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExponentialWeightedAverageTimeToFirstByteEstimatorTest.java
new file mode 100644
index 0000000000..5e263fe1ac
--- /dev/null
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExponentialWeightedAverageTimeToFirstByteEstimatorTest.java
@@ -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);
+ }
+}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/PercentileTimeToFirstByteEstimatorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/PercentileTimeToFirstByteEstimatorTest.java
new file mode 100644
index 0000000000..72c38a01b4
--- /dev/null
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/PercentileTimeToFirstByteEstimatorTest.java
@@ -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);
+ }
+}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/SlidingPercentileBandwidthStatisticTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/SlidingPercentileBandwidthStatisticTest.java
new file mode 100644
index 0000000000..62e2d1e719
--- /dev/null
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/SlidingPercentileBandwidthStatisticTest.java
@@ -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);
+ }
+}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/SlidingWeightedAverageBandwidthStatisticTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/SlidingWeightedAverageBandwidthStatisticTest.java
new file mode 100644
index 0000000000..902c8b765c
--- /dev/null
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/SlidingWeightedAverageBandwidthStatisticTest.java
@@ -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);
+ }
+}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/SplitParallelSampleBandwidthEstimatorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/SplitParallelSampleBandwidthEstimatorTest.java
new file mode 100644
index 0000000000..6287af6017
--- /dev/null
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/SplitParallelSampleBandwidthEstimatorTest.java
@@ -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);
+ }
+}