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 9c342b6065..fdfd87910e 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 @@ -17,8 +17,12 @@ package com.google.android.exoplayer2.trackselection; 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.source.chunk.MediaChunkListIterator; import com.google.android.exoplayer2.util.Assertions; +import java.util.Arrays; +import java.util.List; /** Track selection related utility methods. */ public final class TrackSelectionUtil { @@ -58,10 +62,11 @@ public final class TrackSelectionUtil { } /** - * Returns average bitrate values for a set of tracks whose upcoming media chunk iterators and - * formats are given. If an average bitrate can't be calculated, an estimation is calculated using - * average bitrate of another track and the ratio of the bitrate values defined in the formats of - * the two tracks. + * Returns bitrate values for a set of tracks whose upcoming media chunk iterators and formats are + * given. + * + *
If an average bitrate can't be calculated, an estimation is calculated using average bitrate + * of another track and the ratio of the bitrate values defined in the formats of the two tracks. * * @param iterators An array of {@link MediaChunkIterator}s providing information about the * sequence of upcoming media chunks for each track. @@ -72,7 +77,7 @@ public final class TrackSelectionUtil { * estimation can't be calculated, {@link Format#NO_VALUE} is set. * @see #getAverageBitrate(MediaChunkIterator, long) */ - public static int[] getAverageBitrates( + public static int[] getBitratesUsingFutureInfo( MediaChunkIterator[] iterators, Format[] formats, long maxDurationUs) { int trackCount = iterators.length; Assertions.checkArgument(trackCount == formats.length); @@ -102,26 +107,82 @@ public final class TrackSelectionUtil { } if (needEstimateBitrate && canEstimateBitrate) { - for (int i = 0; i < trackCount; i++) { - if (bitrates[i] == Format.NO_VALUE) { - int formatBitrate = formats[i].bitrate; - if (formatBitrate != Format.NO_VALUE) { - int closestFormat = findClosestBitrateFormat(formatBitrate, formatBitrates); - bitrates[i] = (int) (bitrateRatios[closestFormat] * formatBitrate); - } - } - } + estimateBitrates(bitrates, formats, formatBitrates, bitrateRatios); } return bitrates; } - private static int findClosestBitrateFormat(int formatBitrate, int[] formatBitrates) { + /** + * Returns bitrate values for a set of tracks whose formats are given, using the given queue of + * already buffered {@link MediaChunk} instances. + * + * @param queue The queue of already buffered {@link MediaChunk} instances. Must not be modified. + * @param formats The track formats. + * @param maxDurationUs Maximum duration of chunks to be included in average bitrate values, in + * microseconds. + * @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) + */ + public static int[] getBitratesUsingPastInfo( + List extends MediaChunk> queue, Format[] formats, long maxDurationUs) { + int[] bitrates = new int[formats.length]; + Arrays.fill(bitrates, Format.NO_VALUE); + int queueAverageBitrate = getAverageQueueBitrate(queue, maxDurationUs); + if (queueAverageBitrate == Format.NO_VALUE) { + return bitrates; + } + int queueFormatBitrate = queue.get(queue.size() - 1).trackFormat.bitrate; + if (queueFormatBitrate != Format.NO_VALUE) { + float queueBitrateRatio = ((float) queueAverageBitrate) / queueFormatBitrate; + estimateBitrates( + bitrates, formats, new int[] {queueFormatBitrate}, new float[] {queueBitrateRatio}); + } + return bitrates; + } + + private static int getAverageQueueBitrate(List extends MediaChunk> queue, long maxDurationUs) { + if (queue.isEmpty()) { + return Format.NO_VALUE; + } + MediaChunkListIterator iterator = + new MediaChunkListIterator(getSingleFormatSubQueue(queue), /* reverseOrder= */ true); + return getAverageBitrate(iterator, maxDurationUs); + } + + private static List extends MediaChunk> getSingleFormatSubQueue( + List extends MediaChunk> queue) { + Format queueFormat = queue.get(queue.size() - 1).trackFormat; + int queueSize = queue.size(); + for (int i = queueSize - 2; i >= 0; i--) { + if (!queue.get(i).trackFormat.equals(queueFormat)) { + return queue.subList(i + 1, queueSize); + } + } + return queue; + } + + private static void estimateBitrates( + int[] bitrates, Format[] formats, int[] formatBitrates, float[] bitrateRatios) { + for (int i = 0; i < bitrates.length; i++) { + if (bitrates[i] == Format.NO_VALUE) { + int formatBitrate = formats[i].bitrate; + if (formatBitrate != Format.NO_VALUE) { + int closestFormat = getClosestBitrateIndex(formatBitrate, formatBitrates); + bitrates[i] = (int) (bitrateRatios[closestFormat] * formatBitrate); + } + } + } + } + + private static int getClosestBitrateIndex(int formatBitrate, int[] formatBitrates) { int closestDistance = Integer.MAX_VALUE; int closestFormat = C.INDEX_UNSET; for (int j = 0; j < formatBitrates.length; j++) { if (formatBitrates[j] != Format.NO_VALUE) { int distance = Math.abs(formatBitrates[j] - formatBitrate); if (distance < closestDistance) { + closestDistance = distance; closestFormat = j; } } 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 2d5f2a7c67..49da9b567a 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 @@ -18,12 +18,14 @@ package com.google.android.exoplayer2.trackselection; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; -import android.support.annotation.NonNull; 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.upstream.DataSpec; +import java.util.Arrays; +import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -96,11 +98,12 @@ 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); - assertThat(TrackSelectionUtil.getAverageBitrate(iterator, 30 * C.MICROS_PER_SECOND)) - .isEqualTo(12); + long maxDurationUs = 30 * C.MICROS_PER_SECOND; + int averageBitrate = TrackSelectionUtil.getAverageBitrate(iterator, maxDurationUs); + + assertThat(averageBitrate).isEqualTo(12); } @Test @@ -115,26 +118,26 @@ public class TrackSelectionUtilTest { } @Test - public void getAverageBitrates_noIterator_returnsEmptyArray() { + public void getBitratesUsingFutureInfo_noIterator_returnsEmptyArray() { assertThat( - TrackSelectionUtil.getAverageBitrates( + TrackSelectionUtil.getBitratesUsingFutureInfo( new MediaChunkIterator[0], new Format[0], MAX_DURATION_US)) .hasLength(0); } @Test - public void getAverageBitrates_emptyIterator_returnsNoValue() { - int[] averageBitrates = - TrackSelectionUtil.getAverageBitrates( + public void getBitratesUsingFutureInfo_emptyIterator_returnsNoValue() { + int[] bitrates = + TrackSelectionUtil.getBitratesUsingFutureInfo( new MediaChunkIterator[] {MediaChunkIterator.EMPTY}, new Format[] {createFormatWithBitrate(10)}, MAX_DURATION_US); - assertThat(averageBitrates).asList().containsExactly(Format.NO_VALUE); + assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); } @Test - public void getAverageBitrates_twoTracks_returnsAverageChunkBitrates() { + public void getBitratesUsingFutureInfo_twoTracksZeroMaxDuration_returnsNoValue() { FakeIterator iterator1 = new FakeIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10}); @@ -143,35 +146,62 @@ public class TrackSelectionUtilTest { /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30}, /* chunkLengths= */ new long[] {10, 20, 30}); - int[] averageBitrates = - TrackSelectionUtil.getAverageBitrates( + int[] bitrates = + TrackSelectionUtil.getBitratesUsingFutureInfo( + new MediaChunkIterator[] {iterator1, iterator2}, + new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, + /* maxDurationUs= */ 0); + + assertThat(bitrates).asList().containsExactly(Format.NO_VALUE, Format.NO_VALUE); + } + + @Test + public void getBitratesUsingFutureInfo_twoTracks_returnsBitrates() { + FakeIterator iterator1 = + new FakeIterator( + /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10}); + FakeIterator iterator2 = + new FakeIterator( + /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30}, + /* chunkLengths= */ new long[] {10, 20, 30}); + + int[] bitrates = + TrackSelectionUtil.getBitratesUsingFutureInfo( new MediaChunkIterator[] {iterator1, iterator2}, new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)}, MAX_DURATION_US); - assertThat(averageBitrates).asList().containsExactly(8, 16).inOrder(); + assertThat(bitrates).asList().containsExactly(8, 16).inOrder(); } @Test - public void getAverageBitrates_oneEmptyIteratorOneWithChunks_returnsEstimationForEmpty() { + public void getBitratesUsingFutureInfo_emptyIterator_returnsEstimationUsingClosest() { FakeIterator iterator1 = new FakeIterator( /* 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( + /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {50}); + Format format3 = createFormatWithBitrate(25); + FakeIterator iterator4 = + new FakeIterator( + /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {20}); + Format format4 = createFormatWithBitrate(30); - int[] averageBitrates = - TrackSelectionUtil.getAverageBitrates( - new MediaChunkIterator[] {iterator1, iterator2}, - new Format[] {format1, format2}, + int[] bitrates = + TrackSelectionUtil.getBitratesUsingFutureInfo( + new MediaChunkIterator[] {iterator1, iterator2, iterator3, iterator4}, + new Format[] {format1, format2, format3, format4}, MAX_DURATION_US); - assertThat(averageBitrates).asList().containsExactly(16, 32).inOrder(); + assertThat(bitrates).asList().containsExactly(16, 64, 80, 32).inOrder(); } @Test - public void getAverageBitrates_formatWithoutBitrate_returnsNoValueForEmpty() { + public void getBitratesUsingFutureInfo_formatWithoutBitrate_returnsNoValueForEmpty() { FakeIterator iterator1 = new FakeIterator( /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {10}); @@ -179,16 +209,212 @@ public class TrackSelectionUtilTest { MediaChunkIterator iterator2 = MediaChunkIterator.EMPTY; Format format2 = createFormatWithBitrate(Format.NO_VALUE); - int[] averageBitrates = - TrackSelectionUtil.getAverageBitrates( + int[] bitrates = + TrackSelectionUtil.getBitratesUsingFutureInfo( new MediaChunkIterator[] {iterator1, iterator2}, new Format[] {format1, format2}, MAX_DURATION_US); - assertThat(averageBitrates).asList().containsExactly(16, Format.NO_VALUE).inOrder(); + assertThat(bitrates).asList().containsExactly(16, Format.NO_VALUE).inOrder(); + } + + @Test + public void getBitratesUsingPastInfo_noFormat_returnsEmptyArray() { + FakeMediaChunk chunk = + createChunk( + createFormatWithBitrate(10), + /* length= */ 10, + /* startTimeSec= */ 0, + /* endTimeSec= */ 10); + + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Collections.singletonList(chunk), new Format[0], MAX_DURATION_US); + + assertThat(bitrates).hasLength(0); + } + + @Test + public void getBitratesUsingPastInfo_emptyQueue_returnsNoValue() { + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Collections.emptyList(), new Format[] {createFormatWithBitrate(10)}, MAX_DURATION_US); + + assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); + } + + @Test + public void getBitratesUsingPastInfo_oneChunkFormatNoBitrate_returnsNoValue() { + Format format = createFormatWithBitrate(Format.NO_VALUE); + FakeMediaChunk chunk = + createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10); + + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Collections.singletonList(chunk), new Format[] {format}, MAX_DURATION_US); + + assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); + } + + @Test + public void getBitratesUsingPastInfo_oneChunkNoLength_returnsNoValue() { + Format format = createFormatWithBitrate(10); + FakeMediaChunk chunk = + createChunk( + format, /* length= */ C.LENGTH_UNSET, /* startTimeSec= */ 0, /* endTimeSec= */ 10); + + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Collections.singletonList(chunk), new Format[] {format}, MAX_DURATION_US); + + assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); + } + + @Test + public void getBitratesUsingPastInfo_oneChunkWithSameFormat_returnsBitrates() { + Format format = createFormatWithBitrate(10); + FakeMediaChunk chunk = + createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10); + + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Collections.singletonList(chunk), new Format[] {format}, MAX_DURATION_US); + + assertThat(bitrates).asList().containsExactly(8).inOrder(); + } + + @Test + public void getBitratesUsingPastInfo_zeroMaxDuration_returnsNoValue() { + Format format = createFormatWithBitrate(10); + FakeMediaChunk chunk = + createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10); + + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Collections.singletonList(chunk), new Format[] {format}, /* maxDurationUs= */ 0); + + assertThat(bitrates).asList().containsExactly(Format.NO_VALUE).inOrder(); + } + + @Test + public void getBitratesUsingPastInfo_multipleChunkWithSameFormat_returnsAverageBitrate() { + Format format = createFormatWithBitrate(10); + FakeMediaChunk chunk = + createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10); + FakeMediaChunk chunk2 = + createChunk(format, /* length= */ 20, /* startTimeSec= */ 10, /* endTimeSec= */ 20); + + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Arrays.asList(chunk, chunk2), new Format[] {format}, MAX_DURATION_US); + + assertThat(bitrates).asList().containsExactly(12).inOrder(); + } + + @Test + public void getBitratesUsingPastInfo_oneChunkWithDifferentFormat_returnsEstimationBitrate() { + FakeMediaChunk chunk = + createChunk( + createFormatWithBitrate(10), + /* length= */ 10, + /* startTimeSec= */ 0, + /* endTimeSec= */ 10); + + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Collections.singletonList(chunk), + new Format[] {createFormatWithBitrate(20)}, + MAX_DURATION_US); + + assertThat(bitrates).asList().containsExactly(16).inOrder(); + } + + @Test + public void getBitratesUsingPastInfo_trackFormatNoBitrate_returnsNoValue() { + FakeMediaChunk chunk = + createChunk( + createFormatWithBitrate(10), + /* length= */ 10, + /* startTimeSec= */ 0, + /* endTimeSec= */ 10); + + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Collections.singletonList(chunk), + new Format[] {createFormatWithBitrate(Format.NO_VALUE)}, + MAX_DURATION_US); + + assertThat(bitrates).asList().containsExactly(Format.NO_VALUE); + } + + @Test + public void getBitratesUsingPastInfo_multipleTracks_returnsBitrates() { + FakeMediaChunk chunk = + createChunk( + createFormatWithBitrate(10), + /* length= */ 10, + /* startTimeSec= */ 0, + /* endTimeSec= */ 10); + + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Collections.singletonList(chunk), + new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)}, + MAX_DURATION_US); + + assertThat(bitrates).asList().containsExactly(16, 24).inOrder(); + } + + @Test + public void + getBitratesUsingPastInfo_multipleChunkExceedingMaxDuration_returnsAverageUntilMaxDuration() { + Format format = createFormatWithBitrate(10); + FakeMediaChunk chunk = + createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 20); + FakeMediaChunk chunk2 = + createChunk(format, /* length= */ 40, /* startTimeSec= */ 20, /* endTimeSec= */ 40); + + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Arrays.asList(chunk, chunk2), + new Format[] {format}, + /* maxDurationUs= */ 30 * C.MICROS_PER_SECOND); + + assertThat(bitrates).asList().containsExactly(12).inOrder(); + } + + @Test + public void + getBitratesUsingPastInfo_chunksWithDifferentFormats_returnsChunkAverageBitrateForLastFormat() { + FakeMediaChunk chunk = + createChunk( + createFormatWithBitrate(10), + /* length= */ 10, + /* startTimeSec= */ 0, + /* endTimeSec= */ 10); + FakeMediaChunk chunk2 = + createChunk( + createFormatWithBitrate(20), + /* length= */ 40, + /* startTimeSec= */ 10, + /* endTimeSec= */ 20); + + int[] bitrates = + TrackSelectionUtil.getBitratesUsingPastInfo( + Arrays.asList(chunk, chunk2), + new Format[] {createFormatWithBitrate(10)}, + MAX_DURATION_US); + + assertThat(bitrates).asList().containsExactly(16).inOrder(); + } + + private static FakeMediaChunk createChunk( + Format format, int length, int startTimeSec, int endTimeSec) { + DataSpec dataSpec = new DataSpec(Uri.EMPTY, 0, length, null, 0); + return new FakeMediaChunk( + dataSpec, format, startTimeSec * C.MICROS_PER_SECOND, endTimeSec * C.MICROS_PER_SECOND); } - @NonNull private static Format createFormatWithBitrate(int bitrate) { return Format.createSampleFormat(null, null, null, bitrate, null); } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java index 8740e3e57f..6669504c07 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java @@ -30,9 +30,13 @@ public final class FakeMediaChunk extends MediaChunk { private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT", null); public FakeMediaChunk(Format trackFormat, long startTimeUs, long endTimeUs) { + this(new DataSpec(Uri.EMPTY), trackFormat, startTimeUs, endTimeUs); + } + + public FakeMediaChunk(DataSpec dataSpec, Format trackFormat, long startTimeUs, long endTimeUs) { super( DATA_SOURCE, - new DataSpec(Uri.EMPTY), + dataSpec, trackFormat, C.SELECTION_REASON_ADAPTIVE, /* trackSelectionData= */ null,