Preacquire DRM sessions from the loading side of SampleQueue

Issue: #4133
PiperOrigin-RevId: 362478801
This commit is contained in:
ibaker 2021-03-12 10:20:24 +00:00 committed by Ian Baker
parent f8fb9dd606
commit 795ddfee40
5 changed files with 240 additions and 85 deletions

View File

@ -47,6 +47,8 @@
video tracks (previously separate acquire and release events were video tracks (previously separate acquire and release events were
dispatched for each track in each period). dispatched for each track in each period).
* Include the session state in DRM session-acquired listener methods. * Include the session state in DRM session-acquired listener methods.
* Prepare DRM sessions (and fetch keys) ahead of the playback position
([#4133](https://github.com/google/ExoPlayer/issues/4133)).
* Text * Text
* Parse SSA/ASS bold & italic info in `Style:` lines * Parse SSA/ASS bold & italic info in `Style:` lines
([#8435](https://github.com/google/ExoPlayer/issues/8435)). ([#8435](https://github.com/google/ExoPlayer/issues/8435)).
@ -65,8 +67,8 @@
media item and so that it is not triggered after a timeline change. media item and so that it is not triggered after a timeline change.
* Trigger `onMediaItemTransition` event for all reasons except * Trigger `onMediaItemTransition` event for all reasons except
`MEDIA_ITEM_TRANSITION_REASON_REPEAT`. `MEDIA_ITEM_TRANSITION_REASON_REPEAT`.
* Allow the use of platform extractors through [MediaParser] * Allow the use of platform extractors through
(https://developer.android.com/reference/android/media/MediaParser). [MediaParser](https://developer.android.com/reference/android/media/MediaParser).
Only supported on API 30+. Only supported on API 30+.
* You can use it for progressive media by passing a * You can use it for progressive media by passing a
`MediaParserExtractorAdapter.FACTORY` when creating the `MediaParserExtractorAdapter.FACTORY` when creating the

View File

@ -34,6 +34,7 @@ import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager.DrmSessionReference;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataReader; import com.google.android.exoplayer2.upstream.DataReader;
@ -63,7 +64,7 @@ public class SampleQueue implements TrackOutput {
private final SampleDataQueue sampleDataQueue; private final SampleDataQueue sampleDataQueue;
private final SampleExtrasHolder extrasHolder; private final SampleExtrasHolder extrasHolder;
private final SpannedData<Format> formatSpans; private final SpannedData<SharedSampleMetadata> sharedSampleMetadata;
@Nullable private final DrmSessionManager drmSessionManager; @Nullable private final DrmSessionManager drmSessionManager;
@Nullable private final DrmSessionEventListener.EventDispatcher drmEventDispatcher; @Nullable private final DrmSessionEventListener.EventDispatcher drmEventDispatcher;
@Nullable private final Looper playbackLooper; @Nullable private final Looper playbackLooper;
@ -156,7 +157,8 @@ public class SampleQueue implements TrackOutput {
flags = new int[capacity]; flags = new int[capacity];
sizes = new int[capacity]; sizes = new int[capacity];
cryptoDatas = new CryptoData[capacity]; cryptoDatas = new CryptoData[capacity];
formatSpans = new SpannedData<>(); sharedSampleMetadata =
new SpannedData<>(/* removeCallback= */ metadata -> metadata.drmSessionReference.release());
startTimeUs = Long.MIN_VALUE; startTimeUs = Long.MIN_VALUE;
largestDiscardedTimestampUs = Long.MIN_VALUE; largestDiscardedTimestampUs = Long.MIN_VALUE;
largestQueuedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE;
@ -198,7 +200,7 @@ public class SampleQueue implements TrackOutput {
largestDiscardedTimestampUs = Long.MIN_VALUE; largestDiscardedTimestampUs = Long.MIN_VALUE;
largestQueuedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE;
isLastSampleQueued = false; isLastSampleQueued = false;
formatSpans.clear(); sharedSampleMetadata.clear();
if (resetUpstreamFormat) { if (resetUpstreamFormat) {
unadjustedUpstreamFormat = null; unadjustedUpstreamFormat = null;
upstreamFormat = null; upstreamFormat = null;
@ -371,7 +373,7 @@ public class SampleQueue implements TrackOutput {
|| isLastSampleQueued || isLastSampleQueued
|| (upstreamFormat != null && upstreamFormat != downstreamFormat); || (upstreamFormat != null && upstreamFormat != downstreamFormat);
} }
if (formatSpans.get(getReadIndex()) != downstreamFormat) { if (sharedSampleMetadata.get(getReadIndex()).format != downstreamFormat) {
// A format can be read. // A format can be read.
return true; return true;
} }
@ -690,7 +692,7 @@ public class SampleQueue implements TrackOutput {
} }
} }
Format format = formatSpans.get(getReadIndex()); Format format = sharedSampleMetadata.get(getReadIndex()).format;
if (formatRequired || format != downstreamFormat) { if (formatRequired || format != downstreamFormat) {
onFormatResult(format, formatHolder); onFormatResult(format, formatHolder);
return C.RESULT_FORMAT_READ; return C.RESULT_FORMAT_READ;
@ -723,7 +725,10 @@ public class SampleQueue implements TrackOutput {
return false; return false;
} }
@Nullable Format upstreamCommittedFormat = formatSpans.getEndValue(); @Nullable SharedSampleMetadata upstreamCommittedMetadata = sharedSampleMetadata.getEndValue();
@Nullable
Format upstreamCommittedFormat =
upstreamCommittedMetadata != null ? upstreamCommittedMetadata.format : null;
if (Util.areEqual(format, upstreamCommittedFormat)) { if (Util.areEqual(format, upstreamCommittedFormat)) {
// The format has changed back to the format of the last committed sample. If they are // 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 // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat
@ -799,8 +804,18 @@ public class SampleQueue implements TrackOutput {
cryptoDatas[relativeEndIndex] = cryptoData; cryptoDatas[relativeEndIndex] = cryptoData;
sourceIds[relativeEndIndex] = upstreamSourceId; sourceIds[relativeEndIndex] = upstreamSourceId;
if (!Util.areEqual(upstreamFormat, formatSpans.getEndValue())) { @Nullable SharedSampleMetadata upstreamCommittedMetadata = sharedSampleMetadata.getEndValue();
formatSpans.appendSpan(getWriteIndex(), checkNotNull(upstreamFormat)); if (upstreamCommittedMetadata == null
|| !upstreamCommittedMetadata.format.equals(upstreamFormat)) {
DrmSessionReference drmSessionReference =
drmSessionManager != null
? drmSessionManager.preacquireSession(
checkNotNull(playbackLooper), drmEventDispatcher, upstreamFormat)
: DrmSessionReference.EMPTY;
sharedSampleMetadata.appendSpan(
getWriteIndex(),
new SharedSampleMetadata(checkNotNull(upstreamFormat), drmSessionReference));
} }
length++; length++;
@ -863,7 +878,7 @@ public class SampleQueue implements TrackOutput {
length -= discardCount; length -= discardCount;
largestQueuedTimestampUs = max(largestDiscardedTimestampUs, getLargestTimestamp(length)); largestQueuedTimestampUs = max(largestDiscardedTimestampUs, getLargestTimestamp(length));
isLastSampleQueued = discardCount == 0 && isLastSampleQueued; isLastSampleQueued = discardCount == 0 && isLastSampleQueued;
formatSpans.discardFrom(discardFromIndex); sharedSampleMetadata.discardFrom(discardFromIndex);
if (length != 0) { if (length != 0) {
int relativeLastWriteIndex = getRelativeIndex(length - 1); int relativeLastWriteIndex = getRelativeIndex(length - 1);
return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex]; return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex];
@ -1003,7 +1018,7 @@ public class SampleQueue implements TrackOutput {
if (readPosition < 0) { if (readPosition < 0) {
readPosition = 0; readPosition = 0;
} }
formatSpans.discardTo(absoluteFirstIndex); sharedSampleMetadata.discardTo(absoluteFirstIndex);
if (length == 0) { if (length == 0) {
int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1; int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1;
@ -1057,4 +1072,15 @@ public class SampleQueue implements TrackOutput {
public long offset; public long offset;
@Nullable public CryptoData cryptoData; @Nullable public CryptoData cryptoData;
} }
/** A holder for metadata that applies to a span of contiguous samples. */
private static final class SharedSampleMetadata {
public final Format format;
public final DrmSessionReference drmSessionReference;
private SharedSampleMetadata(Format format, DrmSessionReference drmSessionReference) {
this.format = format;
this.drmSessionReference = drmSessionReference;
}
}
} }

View File

@ -22,6 +22,7 @@ import static java.lang.Math.min;
import android.util.SparseArray; import android.util.SparseArray;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Consumer;
/** /**
* Stores value objects associated with spans of integer keys. * Stores value objects associated with spans of integer keys.
@ -39,10 +40,21 @@ import com.google.android.exoplayer2.C;
private int memoizedReadIndex; private int memoizedReadIndex;
private final SparseArray<V> spans; private final SparseArray<V> spans;
private final Consumer<V> removeCallback;
/** Constructs an empty instance. */ /** Constructs an empty instance. */
public SpannedData() { public SpannedData() {
this(/* removeCallback= */ value -> {});
}
/**
* Constructs an empty instance that invokes {@code removeCallback} on each value that is removed
* from the collection.
*/
public SpannedData(Consumer<V> removeCallback) {
spans = new SparseArray<>(); spans = new SparseArray<>();
this.removeCallback = removeCallback;
memoizedReadIndex = C.INDEX_UNSET;
} }
/** /**
@ -71,7 +83,8 @@ import com.google.android.exoplayer2.C;
* Adds a new span to the end starting at {@code startKey} and containing {@code value}. * Adds a new span to the end starting at {@code startKey} and containing {@code value}.
* *
* <p>{@code startKey} must be greater than or equal to the start key of the previous span. If * <p>{@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. * they're equal, the previous span is overwritten and it's passed to {@code removeCallback} (if
* set).
*/ */
public void appendSpan(int startKey, V value) { public void appendSpan(int startKey, V value) {
if (memoizedReadIndex == C.INDEX_UNSET) { if (memoizedReadIndex == C.INDEX_UNSET) {
@ -79,7 +92,13 @@ import com.google.android.exoplayer2.C;
memoizedReadIndex = 0; memoizedReadIndex = 0;
} }
checkArgument(spans.size() == 0 || startKey >= spans.keyAt(spans.size() - 1)); if (spans.size() > 0) {
int lastStartKey = spans.keyAt(spans.size() - 1);
checkArgument(startKey >= lastStartKey);
if (lastStartKey == startKey) {
removeCallback.accept(spans.valueAt(spans.size() - 1));
}
}
spans.append(startKey, value); spans.append(startKey, value);
} }
@ -102,6 +121,7 @@ import com.google.android.exoplayer2.C;
*/ */
public void discardTo(int discardToKey) { public void discardTo(int discardToKey) {
for (int i = 0; i < spans.size() - 1 && discardToKey >= spans.keyAt(i + 1); i++) { for (int i = 0; i < spans.size() - 1 && discardToKey >= spans.keyAt(i + 1); i++) {
removeCallback.accept(spans.valueAt(i));
spans.removeAt(i); spans.removeAt(i);
if (memoizedReadIndex > 0) { if (memoizedReadIndex > 0) {
memoizedReadIndex--; memoizedReadIndex--;
@ -116,6 +136,7 @@ import com.google.android.exoplayer2.C;
*/ */
public void discardFrom(int discardFromKey) { public void discardFrom(int discardFromKey) {
for (int i = spans.size() - 1; i >= 0 && discardFromKey < spans.keyAt(i); i--) { for (int i = spans.size() - 1; i >= 0 && discardFromKey < spans.keyAt(i); i--) {
removeCallback.accept(spans.valueAt(i));
spans.removeAt(i); spans.removeAt(i);
} }
memoizedReadIndex = spans.size() > 0 ? min(memoizedReadIndex, spans.size() - 1) : C.INDEX_UNSET; memoizedReadIndex = spans.size() > 0 ? min(memoizedReadIndex, spans.size() - 1) : C.INDEX_UNSET;
@ -123,6 +144,9 @@ import com.google.android.exoplayer2.C;
/** Remove all spans. */ /** Remove all spans. */
public void clear() { public void clear() {
for (int i = 0; i < spans.size(); i++) {
removeCallback.accept(spans.valueAt(i));
}
memoizedReadIndex = C.INDEX_UNSET; memoizedReadIndex = C.INDEX_UNSET;
spans.clear(); spans.clear();
} }

View File

@ -86,6 +86,7 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm;
import com.google.android.exoplayer2.drm.MediaDrmCallback; import com.google.android.exoplayer2.drm.MediaDrmCallback;
import com.google.android.exoplayer2.drm.MediaDrmCallbackException; import com.google.android.exoplayer2.drm.MediaDrmCallbackException;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.robolectric.RobolectricUtil;
import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper; import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.LoadEventInfo;
@ -108,6 +109,7 @@ import com.google.android.exoplayer2.testutil.TestExoPlayerBuilder;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
@ -1436,19 +1438,43 @@ public final class AnalyticsCollectorTest {
} }
@Test @Test
public void drmEvents_periodWithSameDrmData_keysReused() throws Exception { public void drmEvents_periodsWithSameDrmData_keysReusedButLoadEventReportedTwice()
throws Exception {
BlockingDrmCallback mediaDrmCallback = BlockingDrmCallback.returnsEmpty();
DrmSessionManager blockingDrmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
.setMultiSession(true)
.build(mediaDrmCallback);
MediaSource mediaSource = MediaSource mediaSource =
new ConcatenatingMediaSource( new ConcatenatingMediaSource(
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, drmSessionManager, VIDEO_FORMAT_DRM_1), new FakeMediaSource(
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, drmSessionManager, VIDEO_FORMAT_DRM_1)); SINGLE_PERIOD_TIMELINE, blockingDrmSessionManager, VIDEO_FORMAT_DRM_1),
TestAnalyticsListener listener = runAnalyticsTest(mediaSource); new FakeMediaSource(
SINGLE_PERIOD_TIMELINE, blockingDrmSessionManager, VIDEO_FORMAT_DRM_1));
TestAnalyticsListener listener =
runAnalyticsTest(
mediaSource,
// Wait for the media to be fully buffered before unblocking the DRM key request. This
// ensures both periods report the same load event (because period1's DRM session is
// already preacquired by the time the key load completes).
new ActionSchedule.Builder(TAG)
.waitForIsLoading(false)
.waitForIsLoading(true)
.waitForIsLoading(false)
.executeRunnable(mediaDrmCallback.keyCondition::open)
.build());
populateEventIds(listener.lastReportedTimeline); populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty(); assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty();
assertThat(listener.getEvents(EVENT_DRM_SESSION_ACQUIRED)) assertThat(listener.getEvents(EVENT_DRM_SESSION_ACQUIRED))
.containsExactly(period0, period1) .containsExactly(period0, period1)
.inOrder(); .inOrder();
assertThat(listener.getEvents(EVENT_DRM_KEYS_LOADED)).containsExactly(period0); // This includes both period0 and period1 because period1's DrmSession was preacquired before
// the key load completed. There's only one key load (a second would block forever). We can't
// assume the order these events will arrive in because it depends on the iteration order of a
// HashSet of EventDispatchers inside DefaultDrmSession.
assertThat(listener.getEvents(EVENT_DRM_KEYS_LOADED)).containsExactly(period0, period1);
// The period1 release event is lost because it's posted to "ExoPlayerTest thread" after that // The period1 release event is lost because it's posted to "ExoPlayerTest thread" after that
// thread has been quit during clean-up. // thread has been quit during clean-up.
assertThat(listener.getEvents(EVENT_DRM_SESSION_RELEASED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_DRM_SESSION_RELEASED)).containsExactly(period0);
@ -1480,11 +1506,21 @@ public final class AnalyticsCollectorTest {
@Test @Test
public void drmEvents_errorHandling() throws Exception { public void drmEvents_errorHandling() throws Exception {
BlockingDrmCallback mediaDrmCallback = BlockingDrmCallback.alwaysFailing();
DrmSessionManager failingDrmSessionManager = DrmSessionManager failingDrmSessionManager =
new DefaultDrmSessionManager.Builder().build(new FailingDrmCallback()); new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
.setMultiSession(true)
.build(mediaDrmCallback);
MediaSource mediaSource = MediaSource mediaSource =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, failingDrmSessionManager, VIDEO_FORMAT_DRM_1); new FakeMediaSource(SINGLE_PERIOD_TIMELINE, failingDrmSessionManager, VIDEO_FORMAT_DRM_1);
TestAnalyticsListener listener = runAnalyticsTest(mediaSource); TestAnalyticsListener listener =
runAnalyticsTest(
mediaSource,
new ActionSchedule.Builder(TAG)
.waitForIsLoading(false)
.executeRunnable(mediaDrmCallback.keyCondition::open)
.build());
populateEventIds(listener.lastReportedTimeline); populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).containsExactly(period0); assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).containsExactly(period0);
@ -2341,21 +2377,56 @@ public final class AnalyticsCollectorTest {
} }
/** /**
* A {@link MediaDrmCallback} that throws exceptions for both {@link * A {@link MediaDrmCallback} that blocks each provision and key request until the associated
* #executeProvisionRequest(UUID, ExoMediaDrm.ProvisionRequest)} and {@link * {@link ConditionVariable} field is opened, and then returns an empty byte array. The {@link
* #executeKeyRequest(UUID, ExoMediaDrm.KeyRequest)}. * ConditionVariable} must be explicitly opened for each request.
*/ */
private static final class FailingDrmCallback implements MediaDrmCallback { private static final class BlockingDrmCallback implements MediaDrmCallback {
public final ConditionVariable provisionCondition;
public final ConditionVariable keyCondition;
private final boolean alwaysFail;
private BlockingDrmCallback(boolean alwaysFail) {
this.provisionCondition = RobolectricUtil.createRobolectricConditionVariable();
this.keyCondition = RobolectricUtil.createRobolectricConditionVariable();
this.alwaysFail = alwaysFail;
}
/** Returns a callback that always returns an empty byte array from its execute methods. */
public static BlockingDrmCallback returnsEmpty() {
return new BlockingDrmCallback(/* alwaysFail= */ false);
}
/** Returns a callback that always throws an exception from its execute methods. */
public static BlockingDrmCallback alwaysFailing() {
return new BlockingDrmCallback(/* alwaysFail= */ true);
}
@Override @Override
public byte[] executeProvisionRequest(UUID uuid, ExoMediaDrm.ProvisionRequest request) public byte[] executeProvisionRequest(UUID uuid, ExoMediaDrm.ProvisionRequest request)
throws MediaDrmCallbackException { throws MediaDrmCallbackException {
throw new RuntimeException("executeProvision failed"); provisionCondition.blockUninterruptible();
provisionCondition.close();
if (alwaysFail) {
throw new RuntimeException("executeProvisionRequest failed");
} else {
return new byte[0];
}
} }
@Override @Override
public byte[] executeKeyRequest(UUID uuid, ExoMediaDrm.KeyRequest request) public byte[] executeKeyRequest(UUID uuid, ExoMediaDrm.KeyRequest request)
throws MediaDrmCallbackException { throws MediaDrmCallbackException {
throw new RuntimeException("executeKey failed"); keyCondition.blockUninterruptible();
keyCondition.close();
if (alwaysFail) {
throw new RuntimeException("executeKeyRequest failed");
} else {
return new byte[0];
}
} }
} }
} }

View File

@ -16,129 +16,161 @@
package com.google.android.exoplayer2.source; package com.google.android.exoplayer2.source;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.drm.DrmSessionManager.DrmSessionReference;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Tests for {@link SpannedData}. */ /** Tests for {@link SpannedData}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class SpannedDataTest { public final class SpannedDataTest {
private static final String VALUE_1 = "value 1"; @Rule public final MockitoRule mockito = MockitoJUnit.rule();
private static final String VALUE_2 = "value 2";
private static final String VALUE_3 = "value 3"; @Mock private DrmSessionReference value1;
@Mock private DrmSessionReference value2;
@Mock private DrmSessionReference value3;
@Test @Test
public void appendMultipleSpansThenRead() { public void appendMultipleSpansThenRead() {
SpannedData<String> spannedData = new SpannedData<>(); SpannedData<DrmSessionReference> spannedData =
new SpannedData<>(/* removeCallback= */ DrmSessionReference::release);
spannedData.appendSpan(/* startKey= */ 0, VALUE_1); spannedData.appendSpan(/* startKey= */ 0, value1);
spannedData.appendSpan(/* startKey= */ 2, VALUE_2); spannedData.appendSpan(/* startKey= */ 2, value2);
spannedData.appendSpan(/* startKey= */ 4, VALUE_3); spannedData.appendSpan(/* startKey= */ 4, value3);
assertThat(spannedData.get(0)).isEqualTo(VALUE_1); assertThat(spannedData.get(0)).isEqualTo(value1);
assertThat(spannedData.get(1)).isEqualTo(VALUE_1); assertThat(spannedData.get(1)).isEqualTo(value1);
assertThat(spannedData.get(2)).isEqualTo(VALUE_2); assertThat(spannedData.get(2)).isEqualTo(value2);
assertThat(spannedData.get(3)).isEqualTo(VALUE_2); assertThat(spannedData.get(3)).isEqualTo(value2);
assertThat(spannedData.get(4)).isEqualTo(VALUE_3); assertThat(spannedData.get(4)).isEqualTo(value3);
assertThat(spannedData.get(5)).isEqualTo(VALUE_3); assertThat(spannedData.get(5)).isEqualTo(value3);
verify(value1, never()).release();
verify(value2, never()).release();
verify(value3, never()).release();
} }
@Test @Test
public void append_emptySpansDiscarded() { public void append_emptySpansDiscarded() {
SpannedData<String> spannedData = new SpannedData<>(); SpannedData<DrmSessionReference> spannedData = new SpannedData<>();
spannedData.appendSpan(/* startKey= */ 0, VALUE_1); spannedData.appendSpan(/* startKey= */ 0, value1);
spannedData.appendSpan(/* startKey= */ 2, VALUE_2); spannedData.appendSpan(/* startKey= */ 2, value2);
spannedData.appendSpan(/* startKey= */ 2, VALUE_3); spannedData.appendSpan(/* startKey= */ 2, value3);
assertThat(spannedData.get(0)).isEqualTo(VALUE_1); assertThat(spannedData.get(0)).isEqualTo(value1);
assertThat(spannedData.get(1)).isEqualTo(VALUE_1); assertThat(spannedData.get(1)).isEqualTo(value1);
assertThat(spannedData.get(2)).isEqualTo(VALUE_3); assertThat(spannedData.get(2)).isEqualTo(value3);
assertThat(spannedData.get(3)).isEqualTo(VALUE_3); assertThat(spannedData.get(3)).isEqualTo(value3);
} }
@Test @Test
public void discardTo() { public void discardTo() {
SpannedData<String> spannedData = new SpannedData<>(); SpannedData<DrmSessionReference> spannedData =
new SpannedData<>(/* removeCallback= */ DrmSessionReference::release);
spannedData.appendSpan(/* startKey= */ 0, VALUE_1); spannedData.appendSpan(/* startKey= */ 0, value1);
spannedData.appendSpan(/* startKey= */ 2, VALUE_2); spannedData.appendSpan(/* startKey= */ 2, value2);
spannedData.appendSpan(/* startKey= */ 4, VALUE_3); spannedData.appendSpan(/* startKey= */ 4, value3);
spannedData.discardTo(2); spannedData.discardTo(2);
assertThat(spannedData.get(0)).isEqualTo(VALUE_2); verify(value1).release();
assertThat(spannedData.get(2)).isEqualTo(VALUE_2); verify(value2, never()).release();
assertThat(spannedData.get(0)).isEqualTo(value2);
assertThat(spannedData.get(2)).isEqualTo(value2);
spannedData.discardTo(4); spannedData.discardTo(4);
assertThat(spannedData.get(3)).isEqualTo(VALUE_3); verify(value2).release();
assertThat(spannedData.get(4)).isEqualTo(VALUE_3); verify(value3, never()).release();
assertThat(spannedData.get(3)).isEqualTo(value3);
assertThat(spannedData.get(4)).isEqualTo(value3);
} }
@Test @Test
public void discardTo_prunesEmptySpans() { public void discardTo_prunesEmptySpans() {
SpannedData<String> spannedData = new SpannedData<>(); SpannedData<DrmSessionReference> spannedData = new SpannedData<>();
spannedData.appendSpan(/* startKey= */ 0, VALUE_1); spannedData.appendSpan(/* startKey= */ 0, value1);
spannedData.appendSpan(/* startKey= */ 2, VALUE_2); spannedData.appendSpan(/* startKey= */ 2, value2);
spannedData.appendSpan(/* startKey= */ 2, VALUE_3); spannedData.appendSpan(/* startKey= */ 2, value3);
spannedData.discardTo(2); spannedData.discardTo(2);
assertThat(spannedData.get(0)).isEqualTo(VALUE_3); assertThat(spannedData.get(0)).isEqualTo(value3);
assertThat(spannedData.get(2)).isEqualTo(VALUE_3); assertThat(spannedData.get(2)).isEqualTo(value3);
} }
@Test @Test
public void discardFromThenAppend_keepsValueIfSpanEndsUpNonEmpty() { public void discardFromThenAppend_keepsValueIfSpanEndsUpNonEmpty() {
SpannedData<String> spannedData = new SpannedData<>(); SpannedData<DrmSessionReference> spannedData =
new SpannedData<>(/* removeCallback= */ DrmSessionReference::release);
spannedData.appendSpan(/* startKey= */ 0, VALUE_1); spannedData.appendSpan(/* startKey= */ 0, value1);
spannedData.appendSpan(/* startKey= */ 2, VALUE_2); spannedData.appendSpan(/* startKey= */ 2, value2);
spannedData.appendSpan(/* startKey= */ 4, VALUE_3); spannedData.appendSpan(/* startKey= */ 4, value3);
spannedData.discardFrom(2); spannedData.discardFrom(2);
assertThat(spannedData.getEndValue()).isEqualTo(VALUE_2);
spannedData.appendSpan(/* startKey= */ 3, VALUE_3); verify(value3).release();
assertThat(spannedData.getEndValue()).isEqualTo(value2);
assertThat(spannedData.get(0)).isEqualTo(VALUE_1); spannedData.appendSpan(/* startKey= */ 3, value3);
assertThat(spannedData.get(1)).isEqualTo(VALUE_1);
assertThat(spannedData.get(2)).isEqualTo(VALUE_2); verify(value1, never()).release();
assertThat(spannedData.get(3)).isEqualTo(VALUE_3); verify(value2, never()).release();
assertThat(spannedData.get(0)).isEqualTo(value1);
assertThat(spannedData.get(1)).isEqualTo(value1);
assertThat(spannedData.get(2)).isEqualTo(value2);
assertThat(spannedData.get(3)).isEqualTo(value3);
} }
@Test @Test
public void discardFromThenAppend_prunesEmptySpan() { public void discardFromThenAppend_prunesEmptySpan() {
SpannedData<String> spannedData = new SpannedData<>(); SpannedData<DrmSessionReference> spannedData =
new SpannedData<>(/* removeCallback= */ DrmSessionReference::release);
spannedData.appendSpan(/* startKey= */ 0, VALUE_1); spannedData.appendSpan(/* startKey= */ 0, value1);
spannedData.appendSpan(/* startKey= */ 2, VALUE_2); spannedData.appendSpan(/* startKey= */ 2, value2);
spannedData.discardFrom(2); spannedData.discardFrom(2);
spannedData.appendSpan(/* startKey= */ 2, VALUE_3); verify(value2, never()).release();
assertThat(spannedData.get(0)).isEqualTo(VALUE_1); spannedData.appendSpan(/* startKey= */ 2, value3);
assertThat(spannedData.get(1)).isEqualTo(VALUE_1);
assertThat(spannedData.get(2)).isEqualTo(VALUE_3); verify(value2).release();
assertThat(spannedData.get(0)).isEqualTo(value1);
assertThat(spannedData.get(1)).isEqualTo(value1);
assertThat(spannedData.get(2)).isEqualTo(value3);
} }
@Test @Test
public void clear() { public void clear() {
SpannedData<String> spannedData = new SpannedData<>(); SpannedData<DrmSessionReference> spannedData =
new SpannedData<>(/* removeCallback= */ DrmSessionReference::release);
spannedData.appendSpan(/* startKey= */ 0, VALUE_1); spannedData.appendSpan(/* startKey= */ 0, value1);
spannedData.appendSpan(/* startKey= */ 2, VALUE_2); spannedData.appendSpan(/* startKey= */ 2, value2);
spannedData.clear(); spannedData.clear();
spannedData.appendSpan(/* startKey= */ 1, VALUE_3); verify(value1).release();
verify(value2).release();
assertThat(spannedData.get(0)).isEqualTo(VALUE_3); spannedData.appendSpan(/* startKey= */ 1, value3);
assertThat(spannedData.get(1)).isEqualTo(VALUE_3);
assertThat(spannedData.get(0)).isEqualTo(value3);
assertThat(spannedData.get(1)).isEqualTo(value3);
} }
} }