diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackBitrateEstimator.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackBitrateEstimator.java new file mode 100644 index 0000000000..94513b8250 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackBitrateEstimator.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.trackselection; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; +import java.util.List; + +/** Estimates track bitrate values. */ +public interface TrackBitrateEstimator { + + /** + * A {@link TrackBitrateEstimator} that returns the bitrate values defined in the track formats. + */ + TrackBitrateEstimator DEFAULT = + (formats, queue, iterators, bitrates) -> + TrackSelectionUtil.getFormatBitrates(formats, bitrates); + + /** + * Returns bitrate values for a set of tracks whose formats are given. + * + * @param formats The track formats. + * @param queue The queue of already buffered {@link MediaChunk} instances. Must not be modified. + * @param iterators An array of {@link MediaChunkIterator}s providing information about the + * sequence of upcoming media chunks for each track. + * @param bitrates An array into which the bitrate values will be written. If non-null, this array + * is the one that will be returned. + * @return Bitrate values for the tracks. As long as the format of a track has set bitrate, a + * bitrate value is set in the returned array. Otherwise it might be set to {@link + * Format#NO_VALUE}. + */ + int[] getBitrates( + Format[] formats, + List queue, + MediaChunkIterator[] iterators, + @Nullable int[] bitrates); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java index 052cf36309..947f64be2c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.trackselection; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -74,20 +75,25 @@ public final class TrackSelectionUtil { * @param formats The track formats. * @param maxDurationUs Maximum duration of chunks to be included in average bitrate values, in * microseconds. + * @param bitrates If not null, stores bitrate values in this array. * @return Average bitrate values for the tracks. If for a track, an average bitrate or an * estimation can't be calculated, {@link Format#NO_VALUE} is set. * @see #getAverageBitrate(MediaChunkIterator, long) */ @VisibleForTesting /* package */ static int[] getBitratesUsingFutureInfo( - MediaChunkIterator[] iterators, Format[] formats, long maxDurationUs) { + MediaChunkIterator[] iterators, + Format[] formats, + long maxDurationUs, + @Nullable int[] bitrates) { int trackCount = iterators.length; Assertions.checkArgument(trackCount == formats.length); if (trackCount == 0) { return new int[0]; } - - int[] bitrates = new int[trackCount]; + if (bitrates == null) { + bitrates = new int[trackCount]; + } if (maxDurationUs == 0) { Arrays.fill(bitrates, Format.NO_VALUE); return bitrates; @@ -127,15 +133,22 @@ public final class TrackSelectionUtil { * @param formats The track formats. * @param maxDurationUs Maximum duration of chunks to be included in average bitrate values, in * microseconds. + * @param bitrates If not null, calculates bitrate values only for indexes set to Format.NO_VALUE + * and stores result in this array. * @return Bitrate values for the tracks. If for a track, a bitrate value can't be calculated, * {@link Format#NO_VALUE} is set. - * @see #getBitratesUsingFutureInfo(MediaChunkIterator[], Format[], long) + * @see #getBitratesUsingFutureInfo(MediaChunkIterator[], Format[], long, int[]) */ @VisibleForTesting /* package */ static int[] getBitratesUsingPastInfo( - List queue, Format[] formats, long maxDurationUs) { - int[] bitrates = new int[formats.length]; - Arrays.fill(bitrates, Format.NO_VALUE); + List queue, + Format[] formats, + long maxDurationUs, + @Nullable int[] bitrates) { + if (bitrates == null) { + bitrates = new int[formats.length]; + Arrays.fill(bitrates, Format.NO_VALUE); + } if (maxDurationUs == 0) { return bitrates; } @@ -156,40 +169,58 @@ public final class TrackSelectionUtil { * Returns bitrate values for a set of tracks whose formats are given, using the given upcoming * media chunk iterators and the queue of already buffered {@link MediaChunk}s. * + * @param formats The track formats. + * @param queue The queue of already buffered {@link MediaChunk}s. Must not be modified. + * @param maxPastDurationUs Maximum duration of past chunks to be included in average bitrate + * values, in microseconds. * @param iterators An array of {@link MediaChunkIterator}s providing information about the * sequence of upcoming media chunks for each track. - * @param queue The queue of already buffered {@link MediaChunk}s. Must not be modified. - * @param formats The track formats. * @param maxFutureDurationUs Maximum duration of future chunks to be included in average bitrate * values, in microseconds. - * @param maxPastDurationUs Maximum duration of past chunks to be included in average bitrate - * values, in microseconds. * @param useFormatBitrateAsLowerBound Whether to return the estimated bitrate only if it's higher * than the bitrate of the track's format. - * @return Bitrate values for the tracks. If for a track, a bitrate value can't be calculated, - * {@link Format#NO_VALUE} is set. + * @param bitrates An array into which the bitrate values will be written. If non-null, this array + * is the one that will be returned. + * @return Bitrate values for the tracks. As long as the format of a track has set bitrate, a + * bitrate value is set in the returned array. Otherwise it might be set to {@link + * Format#NO_VALUE}. */ public static int[] getBitratesUsingPastAndFutureInfo( - MediaChunkIterator[] iterators, - List queue, Format[] formats, - long maxFutureDurationUs, + List queue, long maxPastDurationUs, - boolean useFormatBitrateAsLowerBound) { - int[] bitrates = getBitratesUsingFutureInfo(iterators, formats, maxFutureDurationUs); - int[] bitratesUsingPastInfo = getBitratesUsingPastInfo(queue, formats, maxPastDurationUs); + MediaChunkIterator[] iterators, + long maxFutureDurationUs, + boolean useFormatBitrateAsLowerBound, + @Nullable int[] bitrates) { + bitrates = getBitratesUsingFutureInfo(iterators, formats, maxFutureDurationUs, bitrates); + getBitratesUsingPastInfo(queue, formats, maxPastDurationUs, bitrates); for (int i = 0; i < bitrates.length; i++) { int bitrate = bitrates[i]; - if (bitrate == Format.NO_VALUE) { - bitrate = bitratesUsingPastInfo[i]; - } if (bitrate == Format.NO_VALUE || (useFormatBitrateAsLowerBound && formats[i].bitrate != Format.NO_VALUE && bitrate < formats[i].bitrate)) { - bitrate = formats[i].bitrate; + bitrates[i] = formats[i].bitrate; } - bitrates[i] = bitrate; + } + return bitrates; + } + + /** + * Returns an array containing {@link Format#bitrate} values for given each format in order. + * + * @param formats The format array to copy {@link Format#bitrate} values. + * @param bitrates If not null, stores bitrate values in this array. + * @return An array containing {@link Format#bitrate} values for given each format in order. + */ + public static int[] getFormatBitrates(Format[] formats, @Nullable int[] bitrates) { + int trackCount = formats.length; + if (bitrates == null) { + bitrates = new int[trackCount]; + } + for (int i = 0; i < trackCount; i++) { + bitrates[i] = formats[i].bitrate; } return bitrates; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimator.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimator.java new file mode 100644 index 0000000000..960b22e83b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimator.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.trackselection; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; +import java.util.List; + +/** A {@link TrackBitrateEstimator} which derives estimates from a window of time. */ +public final class WindowedTrackBitrateEstimator implements TrackBitrateEstimator { + + private final long maxFutureDurationUs; + private final long maxPastDurationUs; + private final boolean useFormatBitrateAsLowerBound; + + /** + * @param maxFutureDurationUs Maximum duration of future chunks to be included in average bitrate + * values, in microseconds. + * @param maxPastDurationUs Maximum duration of past chunks to be included in average bitrate + * values, in microseconds. + * @param useFormatBitrateAsLowerBound Whether to return the estimated bitrate only if it's higher + * than the bitrate of the track's format. + */ + public WindowedTrackBitrateEstimator( + long maxFutureDurationUs, long maxPastDurationUs, boolean useFormatBitrateAsLowerBound) { + this.maxFutureDurationUs = maxFutureDurationUs; + this.maxPastDurationUs = maxPastDurationUs; + this.useFormatBitrateAsLowerBound = useFormatBitrateAsLowerBound; + } + + @Override + public int[] getBitrates( + Format[] formats, + List queue, + MediaChunkIterator[] iterators, + @Nullable int[] bitrates) { + if (maxFutureDurationUs > 0 || maxPastDurationUs > 0) { + return TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( + formats, + queue, + maxPastDurationUs, + iterators, + maxFutureDurationUs, + useFormatBitrateAsLowerBound, + bitrates); + } + return TrackSelectionUtil.getFormatBitrates(formats, bitrates); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.java index 4596ea5ded..c513b86263 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.java @@ -20,9 +20,9 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.testutil.FakeMediaChunk; +import com.google.android.exoplayer2.testutil.FakeMediaChunkIterator; import com.google.android.exoplayer2.upstream.DataSpec; import java.util.Arrays; import java.util.Collections; @@ -47,7 +47,8 @@ public class TrackSelectionUtilTest { long[] chunkTimeBoundariesSec = {12, 17}; long[] chunkLengths = {10}; - FakeIterator iterator = new FakeIterator(chunkTimeBoundariesSec, chunkLengths); + FakeMediaChunkIterator iterator = + new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)).isEqualTo(16); } @@ -57,7 +58,8 @@ public class TrackSelectionUtilTest { long[] chunkTimeBoundariesSec = {0, 5, 10}; long[] chunkLengths = {10, 20}; - FakeIterator iterator = new FakeIterator(chunkTimeBoundariesSec, chunkLengths); + FakeMediaChunkIterator iterator = + new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)).isEqualTo(24); } @@ -67,7 +69,8 @@ public class TrackSelectionUtilTest { long[] chunkTimeBoundariesSec = {0, 5, 15, 30}; long[] chunkLengths = {10, 20, 30}; - FakeIterator iterator = new FakeIterator(chunkTimeBoundariesSec, chunkLengths); + FakeMediaChunkIterator iterator = + new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)).isEqualTo(16); } @@ -77,7 +80,8 @@ public class TrackSelectionUtilTest { long[] chunkTimeBoundariesSec = {0, 5, 15, 30}; long[] chunkLengths = {C.LENGTH_UNSET, 20, 30}; - FakeIterator iterator = new FakeIterator(chunkTimeBoundariesSec, chunkLengths); + FakeMediaChunkIterator iterator = + new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)) .isEqualTo(Format.NO_VALUE); @@ -88,7 +92,8 @@ public class TrackSelectionUtilTest { long[] chunkTimeBoundariesSec = {0, 5, 15, 30}; long[] chunkLengths = {10, C.LENGTH_UNSET, 30}; - FakeIterator iterator = new FakeIterator(chunkTimeBoundariesSec, chunkLengths); + FakeMediaChunkIterator iterator = + new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)).isEqualTo(16); } @@ -98,7 +103,8 @@ public class TrackSelectionUtilTest { getAverageBitrate_chunksExceedingMaxDuration_returnsAverageChunkBitrateUpToMaxDuration() { long[] chunkTimeBoundariesSec = {0, 5, 15, 45, 50}; long[] chunkLengths = {10, 20, 30, 100}; - FakeIterator iterator = new FakeIterator(chunkTimeBoundariesSec, chunkLengths); + FakeMediaChunkIterator iterator = + new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); long maxDurationUs = 30 * C.MICROS_PER_SECOND; int averageBitrate = TrackSelectionUtil.getAverageBitrate(iterator, maxDurationUs); @@ -111,7 +117,8 @@ public class TrackSelectionUtilTest { long[] chunkTimeBoundariesSec = {0, 5, 10}; long[] chunkLengths = {10, 20}; - FakeIterator iterator = new FakeIterator(chunkTimeBoundariesSec, chunkLengths); + FakeMediaChunkIterator iterator = + new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths); assertThat(TrackSelectionUtil.getAverageBitrate(iterator, /* maxDurationUs= */ 0)) .isEqualTo(Format.NO_VALUE); @@ -121,7 +128,7 @@ public class TrackSelectionUtilTest { public void getBitratesUsingFutureInfo_noIterator_returnsEmptyArray() { assertThat( TrackSelectionUtil.getBitratesUsingFutureInfo( - new MediaChunkIterator[0], new Format[0], MAX_DURATION_US)) + new MediaChunkIterator[0], new Format[0], MAX_DURATION_US, /* bitrates= */ null)) .hasLength(0); } @@ -131,18 +138,19 @@ public class TrackSelectionUtilTest { TrackSelectionUtil.getBitratesUsingFutureInfo( new MediaChunkIterator[] {MediaChunkIterator.EMPTY}, new Format[] {createFormatWithBitrate(10)}, - MAX_DURATION_US); + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); } @Test public void getBitratesUsingFutureInfo_twoTracksZeroMaxDuration_returnsNoValue() { - FakeIterator iterator1 = - new FakeIterator( + FakeMediaChunkIterator iterator1 = + new FakeMediaChunkIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10}); - FakeIterator iterator2 = - new FakeIterator( + FakeMediaChunkIterator iterator2 = + new FakeMediaChunkIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30}, /* chunkLengths= */ new long[] {10, 20, 30}); @@ -150,18 +158,19 @@ public class TrackSelectionUtilTest { TrackSelectionUtil.getBitratesUsingFutureInfo( new MediaChunkIterator[] {iterator1, iterator2}, new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, - /* maxDurationUs= */ 0); + /* maxDurationUs= */ 0, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(Format.NO_VALUE, Format.NO_VALUE); } @Test public void getBitratesUsingFutureInfo_twoTracks_returnsBitrates() { - FakeIterator iterator1 = - new FakeIterator( + FakeMediaChunkIterator iterator1 = + new FakeMediaChunkIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10}); - FakeIterator iterator2 = - new FakeIterator( + FakeMediaChunkIterator iterator2 = + new FakeMediaChunkIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30}, /* chunkLengths= */ new long[] {10, 20, 30}); @@ -169,25 +178,47 @@ public class TrackSelectionUtilTest { TrackSelectionUtil.getBitratesUsingFutureInfo( new MediaChunkIterator[] {iterator1, iterator2}, new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, - MAX_DURATION_US); + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(8, 16).inOrder(); } + @Test + public void getBitratesUsingFutureInfo_bitratesArrayGiven_returnsTheSameArray() { + FakeMediaChunkIterator iterator1 = + new FakeMediaChunkIterator( + /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10}); + FakeMediaChunkIterator iterator2 = + new FakeMediaChunkIterator( + /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30}, + /* chunkLengths= */ new long[] {10, 20, 30}); + + int[] bitratesArrayToUse = new int[2]; + int[] bitrates = + TrackSelectionUtil.getBitratesUsingFutureInfo( + new MediaChunkIterator[] {iterator1, iterator2}, + new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, + MAX_DURATION_US, + bitratesArrayToUse); + + assertThat(bitrates).isSameAs(bitratesArrayToUse); + } + @Test public void getBitratesUsingFutureInfo_emptyIterator_returnsEstimationUsingClosest() { - FakeIterator iterator1 = - new FakeIterator( + FakeMediaChunkIterator iterator1 = + new FakeMediaChunkIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {10}); Format format1 = createFormatWithBitrate(10); MediaChunkIterator iterator2 = MediaChunkIterator.EMPTY; Format format2 = createFormatWithBitrate(20); - FakeIterator iterator3 = - new FakeIterator( + FakeMediaChunkIterator iterator3 = + new FakeMediaChunkIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {50}); Format format3 = createFormatWithBitrate(25); - FakeIterator iterator4 = - new FakeIterator( + FakeMediaChunkIterator iterator4 = + new FakeMediaChunkIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {20}); Format format4 = createFormatWithBitrate(30); @@ -195,15 +226,16 @@ public class TrackSelectionUtilTest { TrackSelectionUtil.getBitratesUsingFutureInfo( new MediaChunkIterator[] {iterator1, iterator2, iterator3, iterator4}, new Format[] {format1, format2, format3, format4}, - MAX_DURATION_US); + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(16, 64, 80, 32).inOrder(); } @Test public void getBitratesUsingFutureInfo_formatWithoutBitrate_returnsNoValueForEmpty() { - FakeIterator iterator1 = - new FakeIterator( + FakeMediaChunkIterator iterator1 = + new FakeMediaChunkIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {10}); Format format1 = createFormatWithBitrate(10); MediaChunkIterator iterator2 = MediaChunkIterator.EMPTY; @@ -213,7 +245,8 @@ public class TrackSelectionUtilTest { TrackSelectionUtil.getBitratesUsingFutureInfo( new MediaChunkIterator[] {iterator1, iterator2}, new Format[] {format1, format2}, - MAX_DURATION_US); + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(16, Format.NO_VALUE).inOrder(); } @@ -229,7 +262,7 @@ public class TrackSelectionUtilTest { int[] bitrates = TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), new Format[0], MAX_DURATION_US); + Collections.singletonList(chunk), new Format[0], MAX_DURATION_US, /* bitrates= */ null); assertThat(bitrates).hasLength(0); } @@ -238,7 +271,10 @@ public class TrackSelectionUtilTest { public void getBitratesUsingPastInfo_emptyQueue_returnsNoValue() { int[] bitrates = TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.emptyList(), new Format[] {createFormatWithBitrate(10)}, MAX_DURATION_US); + Collections.emptyList(), + new Format[] {createFormatWithBitrate(10)}, + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); } @@ -251,7 +287,10 @@ public class TrackSelectionUtilTest { int[] bitrates = TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), new Format[] {format}, MAX_DURATION_US); + Collections.singletonList(chunk), + new Format[] {format}, + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); } @@ -265,7 +304,10 @@ public class TrackSelectionUtilTest { int[] bitrates = TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), new Format[] {format}, MAX_DURATION_US); + Collections.singletonList(chunk), + new Format[] {format}, + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); } @@ -278,7 +320,10 @@ public class TrackSelectionUtilTest { int[] bitrates = TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), new Format[] {format}, MAX_DURATION_US); + Collections.singletonList(chunk), + new Format[] {format}, + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(8).inOrder(); } @@ -291,7 +336,10 @@ public class TrackSelectionUtilTest { int[] bitrates = TrackSelectionUtil.getBitratesUsingPastInfo( - Collections.singletonList(chunk), new Format[] {format}, /* maxDurationUs= */ 0); + Collections.singletonList(chunk), + new Format[] {format}, + /* maxDurationUs= */ 0, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(Format.NO_VALUE).inOrder(); } @@ -306,7 +354,10 @@ public class TrackSelectionUtilTest { int[] bitrates = TrackSelectionUtil.getBitratesUsingPastInfo( - Arrays.asList(chunk, chunk2), new Format[] {format}, MAX_DURATION_US); + Arrays.asList(chunk, chunk2), + new Format[] {format}, + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(12).inOrder(); } @@ -324,7 +375,8 @@ public class TrackSelectionUtilTest { TrackSelectionUtil.getBitratesUsingPastInfo( Collections.singletonList(chunk), new Format[] {createFormatWithBitrate(20)}, - MAX_DURATION_US); + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(16).inOrder(); } @@ -342,7 +394,8 @@ public class TrackSelectionUtilTest { TrackSelectionUtil.getBitratesUsingPastInfo( Collections.singletonList(chunk), new Format[] {createFormatWithBitrate(Format.NO_VALUE)}, - MAX_DURATION_US); + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); } @@ -360,11 +413,32 @@ public class TrackSelectionUtilTest { TrackSelectionUtil.getBitratesUsingPastInfo( Collections.singletonList(chunk), new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)}, - MAX_DURATION_US); + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(16, 24).inOrder(); } + @Test + public void getBitratesUsingPastInfo_bitratesArrayGiven_returnsTheSameArray() { + FakeMediaChunk chunk = + createChunk( + createFormatWithBitrate(10), + /* length= */ 10, + /* startTimeSec= */ 0, + /* endTimeSec= */ 10); + + int[] bitratesArrayToUse = new int[2]; + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Collections.singletonList(chunk), + new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)}, + MAX_DURATION_US, + bitratesArrayToUse); + + assertThat(bitrates).isSameAs(bitratesArrayToUse); + } + @Test public void getBitratesUsingPastInfo_multipleChunkExceedingMaxDuration_returnsAverageUntilMaxDuration() { @@ -378,7 +452,8 @@ public class TrackSelectionUtilTest { TrackSelectionUtil.getBitratesUsingPastInfo( Arrays.asList(chunk, chunk2), new Format[] {format}, - /* maxDurationUs= */ 30 * C.MICROS_PER_SECOND); + /* maxDurationUs= */ 30 * C.MICROS_PER_SECOND, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(12).inOrder(); } @@ -403,29 +478,31 @@ public class TrackSelectionUtilTest { TrackSelectionUtil.getBitratesUsingPastInfo( Arrays.asList(chunk, chunk2), new Format[] {createFormatWithBitrate(10)}, - MAX_DURATION_US); + MAX_DURATION_US, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(16).inOrder(); } @Test public void getBitratesUsingPastAndFutureInfo_noPastInfo_returnsBitratesUsingOnlyFutureInfo() { - FakeIterator iterator1 = - new FakeIterator( + FakeMediaChunkIterator iterator1 = + new FakeMediaChunkIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10}); - FakeIterator iterator2 = - new FakeIterator( + FakeMediaChunkIterator iterator2 = + new FakeMediaChunkIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30}, /* chunkLengths= */ new long[] {10, 20, 30}); int[] bitrates = TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( - new MediaChunkIterator[] {iterator1, iterator2}, - Collections.emptyList(), new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, + Collections.emptyList(), MAX_DURATION_US, + new MediaChunkIterator[] {iterator1, iterator2}, MAX_DURATION_US, - /* useFormatBitrateAsLowerBound= */ false); + /* useFormatBitrateAsLowerBound= */ false, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(8, 16).inOrder(); } @@ -441,12 +518,13 @@ public class TrackSelectionUtilTest { int[] bitrates = TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( - new MediaChunkIterator[] {MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY}, - Collections.singletonList(chunk), new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)}, + Collections.singletonList(chunk), MAX_DURATION_US, + new MediaChunkIterator[] {MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY}, MAX_DURATION_US, - /* useFormatBitrateAsLowerBound= */ false); + /* useFormatBitrateAsLowerBound= */ false, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(16, 24).inOrder(); } @@ -460,22 +538,23 @@ public class TrackSelectionUtilTest { /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10); - FakeIterator iterator1 = - new FakeIterator( + FakeMediaChunkIterator iterator1 = + new FakeMediaChunkIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10}); - FakeIterator iterator2 = - new FakeIterator( + FakeMediaChunkIterator iterator2 = + new FakeMediaChunkIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30}, /* chunkLengths= */ new long[] {10, 20, 30}); int[] bitrates = TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( - new MediaChunkIterator[] {iterator1, iterator2}, - Collections.singletonList(chunk), new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, + Collections.singletonList(chunk), MAX_DURATION_US, + new MediaChunkIterator[] {iterator1, iterator2}, MAX_DURATION_US, - /* useFormatBitrateAsLowerBound= */ false); + /* useFormatBitrateAsLowerBound= */ false, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(8, 16).inOrder(); } @@ -484,12 +563,13 @@ public class TrackSelectionUtilTest { public void getBitratesUsingPastAndFutureInfo_noPastAndFutureInfo_returnsBitratesOfFormats() { int[] bitrates = TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( - new MediaChunkIterator[] {MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY}, - Collections.emptyList(), new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, + Collections.emptyList(), MAX_DURATION_US, + new MediaChunkIterator[] {MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY}, MAX_DURATION_US, - /* useFormatBitrateAsLowerBound= */ false); + /* useFormatBitrateAsLowerBound= */ false, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(10, 20).inOrder(); } @@ -506,58 +586,32 @@ public class TrackSelectionUtilTest { int[] bitrates = TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( - new MediaChunkIterator[] {MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY}, - Collections.singletonList(chunk), new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)}, + Collections.singletonList(chunk), MAX_DURATION_US, + new MediaChunkIterator[] {MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY}, MAX_DURATION_US, - /* useFormatBitrateAsLowerBound= */ true); + /* useFormatBitrateAsLowerBound= */ true, + /* bitrates= */ null); assertThat(bitrates).asList().containsExactly(20, 30).inOrder(); } private static FakeMediaChunk createChunk( Format format, int length, int startTimeSec, int endTimeSec) { - DataSpec dataSpec = new DataSpec(Uri.EMPTY, 0, length, null, 0); + DataSpec dataSpec = + new DataSpec( + Uri.EMPTY, /* absoluteStreamPosition= */ 0, length, /* key= */ null, /* flags= */ 0); return new FakeMediaChunk( dataSpec, format, startTimeSec * C.MICROS_PER_SECOND, endTimeSec * C.MICROS_PER_SECOND); } private static Format createFormatWithBitrate(int bitrate) { - return Format.createSampleFormat(null, null, null, bitrate, null); - } - - private static final class FakeIterator extends BaseMediaChunkIterator { - - private final long[] chunkTimeBoundariesSec; - private final long[] chunkLengths; - - public FakeIterator(long[] chunkTimeBoundariesSec, long[] chunkLengths) { - super(/* fromIndex= */ 0, /* toIndex= */ chunkTimeBoundariesSec.length - 2); - this.chunkTimeBoundariesSec = chunkTimeBoundariesSec; - this.chunkLengths = chunkLengths; - } - - @Override - public DataSpec getDataSpec() { - checkInBounds(); - return new DataSpec( - Uri.EMPTY, - /* absoluteStreamPosition= */ 0, - chunkLengths[(int) getCurrentIndex()], - /* key= */ null); - } - - @Override - public long getChunkStartTimeUs() { - checkInBounds(); - return chunkTimeBoundariesSec[(int) getCurrentIndex()] * C.MICROS_PER_SECOND; - } - - @Override - public long getChunkEndTimeUs() { - checkInBounds(); - return chunkTimeBoundariesSec[(int) getCurrentIndex() + 1] * C.MICROS_PER_SECOND; - } + return Format.createSampleFormat( + /* id= */ null, + /* sampleMimeType= */ null, + /* codecs= */ null, + bitrate, + /* drmInitData= */ null); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimatorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimatorTest.java new file mode 100644 index 0000000000..59c5092fc0 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimatorTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.trackselection; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; +import com.google.android.exoplayer2.testutil.FakeMediaChunk; +import com.google.android.exoplayer2.testutil.FakeMediaChunkIterator; +import com.google.android.exoplayer2.upstream.DataSpec; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** {@link WindowedTrackBitrateEstimator} tests. */ +@RunWith(RobolectricTestRunner.class) +public class WindowedTrackBitrateEstimatorTest { + + private static final long MAX_DURATION_US = 30 * C.MICROS_PER_SECOND; + + @Test + public void getBitrates_zeroMaxDuration_returnsFormatBitrates() { + WindowedTrackBitrateEstimator estimator = + new WindowedTrackBitrateEstimator( + /* maxFutureDurationUs= */ 0, + /* maxPastDurationUs= */ 0, + /* useFormatBitrateAsLowerBound= */ false); + MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10); + MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(8); + MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16); + Format format1 = createFormatWithBitrate(10); + Format format2 = createFormatWithBitrate(20); + + int[] bitrates = + estimator.getBitrates( + new Format[] {format1, format2}, + Collections.singletonList(chunk), + new MediaChunkIterator[] {iterator1, iterator2}, + /* bitrates= */ null); + + assertThat(bitrates).asList().containsExactly(10, 20).inOrder(); + } + + @Test + public void getBitrates_futureMaxDurationSet_returnsEstimateUsingFutureChunks() { + WindowedTrackBitrateEstimator estimator = + new WindowedTrackBitrateEstimator( + MAX_DURATION_US, /* maxPastDurationUs= */ 0, /* useFormatBitrateAsLowerBound= */ false); + MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10); + MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(8); + MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16); + Format format1 = createFormatWithBitrate(10); + Format format2 = createFormatWithBitrate(20); + + int[] bitrates = + estimator.getBitrates( + new Format[] {format1, format2}, + Collections.singletonList(chunk), + new MediaChunkIterator[] {iterator1, iterator2}, + /* bitrates= */ null); + + assertThat(bitrates).asList().containsExactly(8, 16).inOrder(); + } + + @Test + public void getBitrates_pastMaxDurationSet_returnsEstimateUsingPastChunks() { + WindowedTrackBitrateEstimator estimator = + new WindowedTrackBitrateEstimator( + /* maxFutureDurationUs= */ 0, + MAX_DURATION_US, + /* useFormatBitrateAsLowerBound= */ false); + MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10); + MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(8); + MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16); + Format format1 = createFormatWithBitrate(10); + Format format2 = createFormatWithBitrate(20); + + int[] bitrates = + estimator.getBitrates( + new Format[] {format1, format2}, + Collections.singletonList(chunk), + new MediaChunkIterator[] {iterator1, iterator2}, + /* bitrates= */ null); + + assertThat(bitrates).asList().containsExactly(16, 32).inOrder(); + } + + @Test + public void + getBitrates_useFormatBitrateAsLowerBoundSetTrue_returnsEstimateIfOnlyHigherThanFormat() { + WindowedTrackBitrateEstimator estimator = + new WindowedTrackBitrateEstimator( + MAX_DURATION_US, MAX_DURATION_US, /* useFormatBitrateAsLowerBound= */ true); + MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10); + MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(80); + MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16); + Format format1 = createFormatWithBitrate(10); + Format format2 = createFormatWithBitrate(20); + + int[] bitrates = + estimator.getBitrates( + new Format[] {format1, format2}, + Collections.singletonList(chunk), + new MediaChunkIterator[] {iterator1, iterator2}, + /* bitrates= */ null); + + assertThat(bitrates).asList().containsExactly(80, 20).inOrder(); + } + + @Test + public void getBitrates_bitratesArrayGiven_returnsTheSameArray() { + WindowedTrackBitrateEstimator estimator = + new WindowedTrackBitrateEstimator( + MAX_DURATION_US, MAX_DURATION_US, /* useFormatBitrateAsLowerBound= */ true); + MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10); + MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(8); + MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16); + Format format1 = createFormatWithBitrate(10); + Format format2 = createFormatWithBitrate(20); + + int[] bitratesArrayToUse = new int[2]; + int[] bitrates = + estimator.getBitrates( + new Format[] {format1, format2}, + Collections.singletonList(chunk), + new MediaChunkIterator[] {iterator1, iterator2}, + bitratesArrayToUse); + + assertThat(bitrates).isSameAs(bitratesArrayToUse); + } + + private static MediaChunk createMediaChunk(int formatBitrate, int actualBitrate) { + int length = actualBitrate / C.BITS_PER_BYTE; + DataSpec dataSpec = + new DataSpec( + Uri.EMPTY, /* absoluteStreamPosition= */ 0, length, /* key= */ null, /* flags= */ 0); + Format format = createFormatWithBitrate(formatBitrate); + return new FakeMediaChunk( + dataSpec, format, /* startTimeUs= */ 0L, /* endTimeUs= */ C.MICROS_PER_SECOND); + } + + private static Format createFormatWithBitrate(int bitrate) { + return Format.createSampleFormat( + /* id= */ null, + /* sampleMimeType= */ null, + /* codecs= */ null, + bitrate, + /* drmInitData= */ null); + } + + private static MediaChunkIterator createMediaChunkIteratorWithBitrate(int bitrate) { + return new FakeMediaChunkIterator( + /* chunkTimeBoundariesSec= */ new long[] {0, 1}, + /* chunkLengths= */ new long[] {bitrate / C.BITS_PER_BYTE}); + } +} diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java new file mode 100644 index 0000000000..6dce2b5428 --- /dev/null +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.testutil; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; +import com.google.android.exoplayer2.upstream.DataSpec; + +/** Fake {@link com.google.android.exoplayer2.source.chunk.MediaChunkIterator}. */ +public final class FakeMediaChunkIterator extends BaseMediaChunkIterator { + + private final long[] chunkTimeBoundariesSec; + private final long[] chunkLengths; + + /** + * Creates a fake {@link com.google.android.exoplayer2.source.chunk.MediaChunkIterator}. + * + * @param chunkTimeBoundariesSec An array containing the time boundaries where one chunk ends and + * the next one starts. The first value is the start time of the first chunk and the last + * value is the end time of the last chunk. The array should be of length (chunk-count + 1). + * @param chunkLengths An array which contains the length of each chunk, should be of length + * (chunk-count). + */ + public FakeMediaChunkIterator(long[] chunkTimeBoundariesSec, long[] chunkLengths) { + super(/* fromIndex= */ 0, /* toIndex= */ chunkTimeBoundariesSec.length - 2); + this.chunkTimeBoundariesSec = chunkTimeBoundariesSec; + this.chunkLengths = chunkLengths; + } + + @Override + public DataSpec getDataSpec() { + checkInBounds(); + return new DataSpec( + Uri.EMPTY, + /* absoluteStreamPosition= */ 0, + chunkLengths[(int) getCurrentIndex()], + /* key= */ null); + } + + @Override + public long getChunkStartTimeUs() { + checkInBounds(); + return chunkTimeBoundariesSec[(int) getCurrentIndex()] * C.MICROS_PER_SECOND; + } + + @Override + public long getChunkEndTimeUs() { + checkInBounds(); + return chunkTimeBoundariesSec[(int) getCurrentIndex() + 1] * C.MICROS_PER_SECOND; + } +}