From bcb9eb4314303abe21062e78ff60a69a3907ed78 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 9 Mar 2021 16:18:39 +0000 Subject: [PATCH] Switch SampleQueue to store Format in a span structure PiperOrigin-RevId: 361813981 --- .../exoplayer2/source/SampleQueue.java | 37 +++-- .../exoplayer2/source/SpannedData.java | 129 ++++++++++++++++ .../exoplayer2/source/SpannedDataTest.java | 144 ++++++++++++++++++ 3 files changed, 294 insertions(+), 16 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/SpannedData.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/source/SpannedDataTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index d0aa089262..7b504eed4b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -16,11 +16,13 @@ package com.google.android.exoplayer2.source; import static com.google.android.exoplayer2.util.Assertions.checkArgument; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static java.lang.Math.max; import android.os.Looper; import android.util.Log; import androidx.annotation.CallSuper; +import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; @@ -61,6 +63,7 @@ public class SampleQueue implements TrackOutput { private final SampleDataQueue sampleDataQueue; private final SampleExtrasHolder extrasHolder; + private final SpannedData formatSpans; @Nullable private final DrmSessionManager drmSessionManager; @Nullable private final DrmSessionEventListener.EventDispatcher drmEventDispatcher; @Nullable private final Looper playbackLooper; @@ -76,7 +79,6 @@ public class SampleQueue implements TrackOutput { private int[] flags; private long[] timesUs; private @NullableType CryptoData[] cryptoDatas; - private Format[] formats; private int length; private int absoluteFirstIndex; @@ -92,7 +94,6 @@ public class SampleQueue implements TrackOutput { private boolean upstreamFormatAdjustmentRequired; @Nullable private Format unadjustedUpstreamFormat; @Nullable private Format upstreamFormat; - @Nullable private Format upstreamCommittedFormat; private int upstreamSourceId; private boolean upstreamAllSamplesAreSyncSamples; private boolean loggedUnexpectedNonSyncSample; @@ -155,7 +156,7 @@ public class SampleQueue implements TrackOutput { flags = new int[capacity]; sizes = new int[capacity]; cryptoDatas = new CryptoData[capacity]; - formats = new Format[capacity]; + formatSpans = new SpannedData<>(); startTimeUs = Long.MIN_VALUE; largestDiscardedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; @@ -197,7 +198,7 @@ public class SampleQueue implements TrackOutput { largestDiscardedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; isLastSampleQueued = false; - upstreamCommittedFormat = null; + formatSpans.clear(); if (resetUpstreamFormat) { unadjustedUpstreamFormat = null; upstreamFormat = null; @@ -370,12 +371,11 @@ public class SampleQueue implements TrackOutput { || isLastSampleQueued || (upstreamFormat != null && upstreamFormat != downstreamFormat); } - int relativeReadIndex = getRelativeIndex(readPosition); - if (formats[relativeReadIndex] != downstreamFormat) { + if (formatSpans.get(getReadIndex()) != downstreamFormat) { // A format can be read. return true; } - return mayReadSample(relativeReadIndex); + return mayReadSample(getRelativeIndex(readPosition)); } /** Equivalent to {@link #read}, except it never advances the read position. */ @@ -690,12 +690,13 @@ public class SampleQueue implements TrackOutput { } } - int relativeReadIndex = getRelativeIndex(readPosition); - if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { - onFormatResult(formats[relativeReadIndex], formatHolder); + Format format = formatSpans.get(getReadIndex()); + if (formatRequired || format != downstreamFormat) { + onFormatResult(format, formatHolder); return C.RESULT_FORMAT_READ; } + int relativeReadIndex = getRelativeIndex(readPosition); if (!mayReadSample(relativeReadIndex)) { buffer.waitingForKeys = true; return C.RESULT_NOTHING_READ; @@ -721,6 +722,8 @@ public class SampleQueue implements TrackOutput { // referential quality. return false; } + + @Nullable Format upstreamCommittedFormat = formatSpans.getEndValue(); if (Util.areEqual(format, upstreamCommittedFormat)) { // The format has changed back to the format of the last committed sample. If they are // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat @@ -794,9 +797,11 @@ public class SampleQueue implements TrackOutput { sizes[relativeEndIndex] = size; flags[relativeEndIndex] = sampleFlags; cryptoDatas[relativeEndIndex] = cryptoData; - formats[relativeEndIndex] = upstreamFormat; sourceIds[relativeEndIndex] = upstreamSourceId; - upstreamCommittedFormat = upstreamFormat; + + if (!Util.areEqual(upstreamFormat, formatSpans.getEndValue())) { + formatSpans.appendSpan(getWriteIndex(), checkNotNull(upstreamFormat)); + } length++; if (length == capacity) { @@ -808,14 +813,12 @@ public class SampleQueue implements TrackOutput { int[] newFlags = new int[newCapacity]; int[] newSizes = new int[newCapacity]; CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; - Format[] newFormats = new Format[newCapacity]; int beforeWrap = capacity - relativeFirstIndex; System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap); System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap); System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap); System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap); System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap); - System.arraycopy(formats, relativeFirstIndex, newFormats, 0, beforeWrap); System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap); int afterWrap = relativeFirstIndex; System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); @@ -823,14 +826,12 @@ public class SampleQueue implements TrackOutput { System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap); - System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); offsets = newOffsets; timesUs = newTimesUs; flags = newFlags; sizes = newSizes; cryptoDatas = newCryptoDatas; - formats = newFormats; sourceIds = newSourceIds; relativeFirstIndex = 0; capacity = newCapacity; @@ -862,6 +863,7 @@ public class SampleQueue implements TrackOutput { length -= discardCount; largestQueuedTimestampUs = max(largestDiscardedTimestampUs, getLargestTimestamp(length)); isLastSampleQueued = discardCount == 0 && isLastSampleQueued; + formatSpans.discardFrom(discardFromIndex); if (length != 0) { int relativeLastWriteIndex = getRelativeIndex(length - 1); return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex]; @@ -987,6 +989,7 @@ public class SampleQueue implements TrackOutput { * @param discardCount The number of samples to discard. * @return The corresponding offset up to which data should be discarded. */ + @GuardedBy("this") private long discardSamples(int discardCount) { largestDiscardedTimestampUs = max(largestDiscardedTimestampUs, getLargestTimestamp(discardCount)); @@ -1000,6 +1003,8 @@ public class SampleQueue implements TrackOutput { if (readPosition < 0) { readPosition = 0; } + formatSpans.discardTo(absoluteFirstIndex); + if (length == 0) { int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1; return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SpannedData.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SpannedData.java new file mode 100644 index 0000000000..287837b096 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SpannedData.java @@ -0,0 +1,129 @@ +/* + * 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 com.google.android.exoplayer2.source; + +import static com.google.android.exoplayer2.util.Assertions.checkArgument; +import static com.google.android.exoplayer2.util.Assertions.checkState; +import static java.lang.Math.min; + +import android.util.SparseArray; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; + +/** + * Stores value objects associated with spans of integer keys. + * + *

This implementation is optimised for consecutive {@link #get(int)} calls with keys that are + * close to each other in value. + * + *

Spans are defined by their own {@code startKey} (inclusive) and the {@code startKey} of the + * next span (exclusive). The last span is open-ended. + * + * @param The type of values stored in this collection. + */ +/* package */ final class SpannedData { + + private int memoizedReadIndex; + + private final SparseArray spans; + + /** Constructs an empty instance. */ + public SpannedData() { + spans = new SparseArray<>(); + } + + /** + * Returns the value associated with the span covering {@code key}. + * + *

{@link #appendSpan(int, Object)} must have been called at least once since the last call to + * {@link #clear()}. + * + *

{@code key} must be greater than or equal to the previous value passed to {@link + * #discardTo(int)} (or zero after {@link #clear()} has been called). + */ + public V get(int key) { + if (memoizedReadIndex == C.INDEX_UNSET) { + memoizedReadIndex = 0; + } + while (memoizedReadIndex > 0 && key < spans.keyAt(memoizedReadIndex)) { + memoizedReadIndex--; + } + while (memoizedReadIndex < spans.size() - 1 && key >= spans.keyAt(memoizedReadIndex + 1)) { + memoizedReadIndex++; + } + return spans.valueAt(memoizedReadIndex); + } + + /** + * Adds a new span to the end starting at {@code startKey} and containing {@code value}. + * + *

{@code startKey} must be greater than or equal to the start key of the previous span. If + * they're equal, the previous span is overwritten. + */ + public void appendSpan(int startKey, V value) { + if (memoizedReadIndex == C.INDEX_UNSET) { + checkState(spans.size() == 0); + memoizedReadIndex = 0; + } + + checkArgument(spans.size() == 0 || startKey >= spans.keyAt(spans.size() - 1)); + spans.append(startKey, value); + } + + /** + * Returns the value associated with the end span, or null if the collection is empty. + * + *

This is either the last value passed to {@link #appendSpan(int, Object)}, or the value of + * the span covering the index passed to {@link #discardFrom(int)}. + */ + @Nullable + public V getEndValue() { + return spans.size() != 0 ? spans.valueAt(spans.size() - 1) : null; + } + + /** + * Discard the spans from the start up to {@code discardToKey}. + * + *

The span associated with {@code discardToKey} is not discarded (which means the last span is + * never discarded). + */ + public void discardTo(int discardToKey) { + for (int i = 0; i < spans.size() - 1 && discardToKey >= spans.keyAt(i + 1); i++) { + spans.removeAt(i); + if (memoizedReadIndex > 0) { + memoizedReadIndex--; + } + } + } + + /** + * Discard the spans from the end back to {@code discardFromKey}. + * + *

The span associated with {@code discardFromKey} is not discarded. + */ + public void discardFrom(int discardFromKey) { + for (int i = spans.size() - 1; i >= 0 && discardFromKey < spans.keyAt(i); i--) { + spans.removeAt(i); + } + memoizedReadIndex = spans.size() > 0 ? min(memoizedReadIndex, spans.size() - 1) : C.INDEX_UNSET; + } + + /** Remove all spans. */ + public void clear() { + memoizedReadIndex = C.INDEX_UNSET; + spans.clear(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SpannedDataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SpannedDataTest.java new file mode 100644 index 0000000000..c0f48293e3 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SpannedDataTest.java @@ -0,0 +1,144 @@ +/* + * 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. + */ +package com.google.android.exoplayer2.source; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link SpannedData}. */ +@RunWith(AndroidJUnit4.class) +public final class SpannedDataTest { + + private static final String VALUE_1 = "value 1"; + private static final String VALUE_2 = "value 2"; + private static final String VALUE_3 = "value 3"; + + @Test + public void appendMultipleSpansThenRead() { + SpannedData spannedData = new SpannedData<>(); + + spannedData.appendSpan(/* startKey= */ 0, VALUE_1); + spannedData.appendSpan(/* startKey= */ 2, VALUE_2); + spannedData.appendSpan(/* startKey= */ 4, VALUE_3); + + assertThat(spannedData.get(0)).isEqualTo(VALUE_1); + assertThat(spannedData.get(1)).isEqualTo(VALUE_1); + assertThat(spannedData.get(2)).isEqualTo(VALUE_2); + assertThat(spannedData.get(3)).isEqualTo(VALUE_2); + assertThat(spannedData.get(4)).isEqualTo(VALUE_3); + assertThat(spannedData.get(5)).isEqualTo(VALUE_3); + } + + @Test + public void append_emptySpansDiscarded() { + SpannedData spannedData = new SpannedData<>(); + + spannedData.appendSpan(/* startKey= */ 0, VALUE_1); + spannedData.appendSpan(/* startKey= */ 2, VALUE_2); + spannedData.appendSpan(/* startKey= */ 2, VALUE_3); + + assertThat(spannedData.get(0)).isEqualTo(VALUE_1); + assertThat(spannedData.get(1)).isEqualTo(VALUE_1); + assertThat(spannedData.get(2)).isEqualTo(VALUE_3); + assertThat(spannedData.get(3)).isEqualTo(VALUE_3); + } + + @Test + public void discardTo() { + SpannedData spannedData = new SpannedData<>(); + + spannedData.appendSpan(/* startKey= */ 0, VALUE_1); + spannedData.appendSpan(/* startKey= */ 2, VALUE_2); + spannedData.appendSpan(/* startKey= */ 4, VALUE_3); + + spannedData.discardTo(2); + + assertThat(spannedData.get(0)).isEqualTo(VALUE_2); + assertThat(spannedData.get(2)).isEqualTo(VALUE_2); + + spannedData.discardTo(4); + + assertThat(spannedData.get(3)).isEqualTo(VALUE_3); + assertThat(spannedData.get(4)).isEqualTo(VALUE_3); + } + + @Test + public void discardTo_prunesEmptySpans() { + SpannedData spannedData = new SpannedData<>(); + + spannedData.appendSpan(/* startKey= */ 0, VALUE_1); + spannedData.appendSpan(/* startKey= */ 2, VALUE_2); + spannedData.appendSpan(/* startKey= */ 2, VALUE_3); + + spannedData.discardTo(2); + + assertThat(spannedData.get(0)).isEqualTo(VALUE_3); + assertThat(spannedData.get(2)).isEqualTo(VALUE_3); + } + + @Test + public void discardFromThenAppend_keepsValueIfSpanEndsUpNonEmpty() { + SpannedData spannedData = new SpannedData<>(); + + spannedData.appendSpan(/* startKey= */ 0, VALUE_1); + spannedData.appendSpan(/* startKey= */ 2, VALUE_2); + spannedData.appendSpan(/* startKey= */ 4, VALUE_3); + + spannedData.discardFrom(2); + assertThat(spannedData.getEndValue()).isEqualTo(VALUE_2); + + spannedData.appendSpan(/* startKey= */ 3, VALUE_3); + + assertThat(spannedData.get(0)).isEqualTo(VALUE_1); + assertThat(spannedData.get(1)).isEqualTo(VALUE_1); + assertThat(spannedData.get(2)).isEqualTo(VALUE_2); + assertThat(spannedData.get(3)).isEqualTo(VALUE_3); + } + + @Test + public void discardFromThenAppend_prunesEmptySpan() { + SpannedData spannedData = new SpannedData<>(); + + spannedData.appendSpan(/* startKey= */ 0, VALUE_1); + spannedData.appendSpan(/* startKey= */ 2, VALUE_2); + + spannedData.discardFrom(2); + + spannedData.appendSpan(/* startKey= */ 2, VALUE_3); + + assertThat(spannedData.get(0)).isEqualTo(VALUE_1); + assertThat(spannedData.get(1)).isEqualTo(VALUE_1); + assertThat(spannedData.get(2)).isEqualTo(VALUE_3); + } + + @Test + public void clear() { + SpannedData spannedData = new SpannedData<>(); + + spannedData.appendSpan(/* startKey= */ 0, VALUE_1); + spannedData.appendSpan(/* startKey= */ 2, VALUE_2); + + spannedData.clear(); + + spannedData.appendSpan(/* startKey= */ 1, VALUE_3); + + assertThat(spannedData.get(0)).isEqualTo(VALUE_3); + assertThat(spannedData.get(1)).isEqualTo(VALUE_3); + } +}