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); + } +}