Commit b68a6988 by aquilescanta Committed by Oliver Woodman

Make SampleQueue populate FormatHolder.drmSession

Also add unit tests for SampleQueue read for samples with
DRM requirements.

PiperOrigin-RevId: 277037826
parent e0253714
/*
* Copyright (C) 2019 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 android.os.Looper;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Reads from a {@link SampleQueue} and attaches {@link DrmSession} references to the {@link Format
* Formats} of encrypted regions.
*/
public final class DecryptableSampleQueueReader {
private final SampleQueue upstream;
private final DrmSessionManager<?> sessionManager;
private final FormatHolder formatHolder;
private final boolean playClearSamplesWithoutKeys;
private @MonotonicNonNull Format currentFormat;
@Nullable private DrmSession<?> currentSession;
/**
* Creates a sample queue reader.
*
* @param upstream The {@link SampleQueue} from which the created reader will read samples.
* @param sessionManager The {@link DrmSessionManager} that will provide {@link DrmSession
* DrmSessions} for the encrypted regions.
*/
public DecryptableSampleQueueReader(SampleQueue upstream, DrmSessionManager<?> sessionManager) {
this.upstream = upstream;
this.sessionManager = sessionManager;
formatHolder = new FormatHolder();
playClearSamplesWithoutKeys =
(sessionManager.getFlags() & DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS) != 0;
}
/** Releases any resources acquired by this reader. */
public void release() {
if (currentSession != null) {
currentSession.releaseReference();
currentSession = null;
}
}
/**
* Throws an error that's preventing data from being read. Does nothing if no such error exists.
*
* @throws IOException The underlying error.
*/
public void maybeThrowError() throws IOException {
// TODO: Avoid throwing if the DRM error is not preventing a read operation.
if (currentSession != null && currentSession.getState() == DrmSession.STATE_ERROR) {
throw Assertions.checkNotNull(currentSession.getError());
}
}
/**
* Reads from the upstream {@link SampleQueue}, populating {@link FormatHolder#drmSession} if the
* current {@link Format#drmInitData} is not null.
*
* <p>This reader guarantees that any read results are usable by clients. An encrypted sample will
* only be returned along with a {@link FormatHolder#drmSession} that has available keys.
*
* @param outputFormatHolder A {@link FormatHolder} to populate in the case of reading a format.
* {@link FormatHolder#drmSession} will be populated if the read format's {@link
* Format#drmInitData} is not null.
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
* end of the stream. If the end of the stream has been reached, the {@link
* C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. If a {@link
* DecoderInputBuffer#isFlagsOnly() flags-only} buffer is passed, only the buffer flags may be
* populated by this method and the read position of the queue will not change.
* @param formatRequired Whether the caller requires that the format of the stream be read even if
* it's not changing. A sample will never be read if set to true, however it is still possible
* for the end of stream or nothing to be read.
* @param loadingFinished True if an empty queue should be considered the end of the stream.
* @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
* be set if the buffer's timestamp is less than this value.
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
* {@link C#RESULT_BUFFER_READ}.
*/
@SuppressWarnings("ReferenceEquality")
public int read(
FormatHolder outputFormatHolder,
DecoderInputBuffer buffer,
boolean formatRequired,
boolean loadingFinished,
long decodeOnlyUntilUs) {
boolean readFlagFormatRequired = false;
boolean readFlagAllowOnlyClearBuffers = false;
boolean onlyPropagateFormatChanges = false;
if (currentFormat == null || formatRequired) {
readFlagFormatRequired = true;
} else if (sessionManager != DrmSessionManager.DUMMY
&& currentFormat.drmInitData != null
&& Assertions.checkNotNull(currentSession).getState()
!= DrmSession.STATE_OPENED_WITH_KEYS) {
if (playClearSamplesWithoutKeys) {
// Content is encrypted and keys are not available, but clear samples are ok for reading.
readFlagAllowOnlyClearBuffers = true;
} else {
// We must not read any samples, but we may still read a format or the end of stream.
// However, because the formatRequired argument is false, we should not propagate a read
// format unless it is different than the current format.
onlyPropagateFormatChanges = true;
readFlagFormatRequired = true;
}
}
int result =
upstream.read(
formatHolder,
buffer,
readFlagFormatRequired,
readFlagAllowOnlyClearBuffers,
loadingFinished,
decodeOnlyUntilUs);
if (result == C.RESULT_FORMAT_READ) {
if (onlyPropagateFormatChanges && currentFormat == formatHolder.format) {
return C.RESULT_NOTHING_READ;
}
onFormat(Assertions.checkNotNull(formatHolder.format), outputFormatHolder);
}
return result;
}
/**
* Updates the current format and manages any necessary DRM resources.
*
* @param format The format read from upstream.
* @param outputFormatHolder The output {@link FormatHolder}.
*/
private void onFormat(Format format, FormatHolder outputFormatHolder) {
outputFormatHolder.format = format;
boolean isFirstFormat = currentFormat == null;
DrmInitData oldDrmInitData = currentFormat != null ? currentFormat.drmInitData : null;
currentFormat = format;
if (sessionManager == DrmSessionManager.DUMMY) {
// Avoid attempting to acquire a session using the dummy DRM session manager. It's likely that
// the media source creation has not yet been migrated and the renderer can acquire the
// session for the read DRM init data.
// TODO: Remove once renderers are migrated [Internal ref: b/122519809].
return;
}
outputFormatHolder.includesDrmSession = true;
outputFormatHolder.drmSession = currentSession;
if (!isFirstFormat && Util.areEqual(oldDrmInitData, format.drmInitData)) {
// Nothing to do.
return;
}
// Ensure we acquire the new session before releasing the previous one in case the same session
// can be used for both DrmInitData.
DrmSession<?> previousSession = currentSession;
DrmInitData drmInitData = currentFormat.drmInitData;
Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper());
currentSession =
drmInitData != null
? sessionManager.acquireSession(playbackLooper, drmInitData)
: sessionManager.acquirePlaceholderSession(playbackLooper);
outputFormatHolder.drmSession = currentSession;
if (previousSession != null) {
previousSession.releaseReference();
}
}
/** Returns whether there is data available for reading. */
public boolean isReady(boolean loadingFinished) {
@SampleQueue.PeekResult int nextInQueue = upstream.peekNext();
if (nextInQueue == SampleQueue.PEEK_RESULT_NOTHING) {
return loadingFinished;
} else if (nextInQueue == SampleQueue.PEEK_RESULT_FORMAT) {
return true;
} else if (nextInQueue == SampleQueue.PEEK_RESULT_BUFFER_CLEAR) {
return currentSession == null || playClearSamplesWithoutKeys;
} else if (nextInQueue == SampleQueue.PEEK_RESULT_BUFFER_ENCRYPTED) {
return sessionManager == DrmSessionManager.DUMMY
|| Assertions.checkNotNull(currentSession).getState()
== DrmSession.STATE_OPENED_WITH_KEYS;
} else {
throw new IllegalStateException();
}
}
}
......@@ -116,7 +116,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Nullable private SeekMap seekMap;
@Nullable private IcyHeaders icyHeaders;
private SampleQueue[] sampleQueues;
private DecryptableSampleQueueReader[] sampleQueueReaders;
private TrackId[] sampleQueueTrackIds;
private boolean sampleQueuesBuilt;
private boolean prepared;
......@@ -193,7 +192,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
handler = new Handler();
sampleQueueTrackIds = new TrackId[0];
sampleQueues = new SampleQueue[0];
sampleQueueReaders = new DecryptableSampleQueueReader[0];
pendingResetPositionUs = C.TIME_UNSET;
length = C.LENGTH_UNSET;
durationUs = C.TIME_UNSET;
......@@ -206,10 +204,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
// Discard as much as we can synchronously. We only do this if we're prepared, since otherwise
// sampleQueues may still be being modified by the loading thread.
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.discardToEnd();
}
for (DecryptableSampleQueueReader reader : sampleQueueReaders) {
reader.release();
sampleQueue.preRelease();
}
}
loader.release(/* callback= */ this);
......@@ -222,10 +217,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Override
public void onLoaderReleased() {
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.reset();
}
for (DecryptableSampleQueueReader reader : sampleQueueReaders) {
reader.release();
sampleQueue.release();
}
extractorHolder.release();
}
......@@ -461,11 +453,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
// SampleStream methods.
/* package */ boolean isReady(int track) {
return !suppressRead() && sampleQueueReaders[track].isReady(loadingFinished);
return !suppressRead() && sampleQueues[track].isReady(loadingFinished);
}
/* package */ void maybeThrowError(int sampleQueueIndex) throws IOException {
sampleQueueReaders[sampleQueueIndex].maybeThrowError();
sampleQueues[sampleQueueIndex].maybeThrowError();
maybeThrowError();
}
......@@ -483,7 +475,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
maybeNotifyDownstreamFormat(sampleQueueIndex);
int result =
sampleQueueReaders[sampleQueueIndex].read(
sampleQueues[sampleQueueIndex].read(
formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs);
if (result == C.RESULT_NOTHING_READ) {
maybeStartDeferredRetry(sampleQueueIndex);
......@@ -690,7 +682,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return sampleQueues[i];
}
}
SampleQueue trackOutput = new SampleQueue(allocator);
SampleQueue trackOutput = new SampleQueue(allocator, drmSessionManager);
trackOutput.setUpstreamFormatChangeListener(this);
@NullableType
TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1);
......@@ -699,12 +691,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@NullableType SampleQueue[] sampleQueues = Arrays.copyOf(this.sampleQueues, trackCount + 1);
sampleQueues[trackCount] = trackOutput;
this.sampleQueues = Util.castNonNullTypeArray(sampleQueues);
@NullableType
DecryptableSampleQueueReader[] sampleQueueReaders =
Arrays.copyOf(this.sampleQueueReaders, trackCount + 1);
sampleQueueReaders[trackCount] =
new DecryptableSampleQueueReader(this.sampleQueues[trackCount], drmSessionManager);
this.sampleQueueReaders = Util.castNonNullTypeArray(sampleQueueReaders);
return trackOutput;
}
......
......@@ -23,7 +23,6 @@ import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.source.DecryptableSampleQueueReader;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.SampleStream;
......@@ -74,7 +73,6 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
private final ArrayList<BaseMediaChunk> mediaChunks;
private final List<BaseMediaChunk> readOnlyMediaChunks;
private final SampleQueue primarySampleQueue;
private final DecryptableSampleQueueReader primarySampleQueueReader;
private final SampleQueue[] embeddedSampleQueues;
private final BaseMediaChunkOutput mediaChunkOutput;
......@@ -132,14 +130,13 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
int[] trackTypes = new int[1 + embeddedTrackCount];
SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount];
primarySampleQueue = new SampleQueue(allocator);
primarySampleQueueReader =
new DecryptableSampleQueueReader(primarySampleQueue, drmSessionManager);
primarySampleQueue = new SampleQueue(allocator, drmSessionManager);
trackTypes[0] = primaryTrackType;
sampleQueues[0] = primarySampleQueue;
for (int i = 0; i < embeddedTrackCount; i++) {
SampleQueue sampleQueue = new SampleQueue(allocator);
SampleQueue sampleQueue =
new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager());
embeddedSampleQueues[i] = sampleQueue;
sampleQueues[i + 1] = sampleQueue;
trackTypes[i + 1] = embeddedTrackTypes[i];
......@@ -337,19 +334,18 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
public void release(@Nullable ReleaseCallback<T> callback) {
this.releaseCallback = callback;
// Discard as much as we can synchronously.
primarySampleQueue.discardToEnd();
primarySampleQueueReader.release();
primarySampleQueue.preRelease();
for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
embeddedSampleQueue.discardToEnd();
embeddedSampleQueue.preRelease();
}
loader.release(this);
}
@Override
public void onLoaderReleased() {
primarySampleQueue.reset();
primarySampleQueue.release();
for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
embeddedSampleQueue.reset();
embeddedSampleQueue.release();
}
if (releaseCallback != null) {
releaseCallback.onSampleStreamReleased(this);
......@@ -360,13 +356,13 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
@Override
public boolean isReady() {
return !isPendingReset() && primarySampleQueueReader.isReady(loadingFinished);
return !isPendingReset() && primarySampleQueue.isReady(loadingFinished);
}
@Override
public void maybeThrowError() throws IOException {
loader.maybeThrowError();
primarySampleQueueReader.maybeThrowError();
primarySampleQueue.maybeThrowError();
if (!loader.isLoading()) {
chunkSource.maybeThrowError();
}
......@@ -380,7 +376,7 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
}
maybeNotifyPrimaryTrackFormatChanged();
return primarySampleQueueReader.read(
return primarySampleQueue.read(
formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs);
}
......@@ -781,7 +777,6 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
formatHolder,
buffer,
formatRequired,
/* allowOnlyClearBuffers= */ false,
loadingFinished,
decodeOnlyUntilPositionUs);
}
......
......@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
......@@ -195,7 +196,8 @@ public final class PlayerEmsgHandler implements Handler.Callback {
/** Returns a {@link TrackOutput} that emsg messages could be written to. */
public PlayerTrackEmsgHandler newPlayerTrackEmsgHandler() {
return new PlayerTrackEmsgHandler(new SampleQueue(allocator));
return new PlayerTrackEmsgHandler(
new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager()));
}
/** Release this emsg handler. It should not be reused after this call. */
......@@ -376,7 +378,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
formatHolder,
buffer,
/* formatRequired= */ false,
/* allowOnlyClearBuffers= */ false,
/* loadingFinished= */ false,
/* decodeOnlyUntilUs= */ 0);
if (result == C.RESULT_BUFFER_READ) {
......
......@@ -36,7 +36,6 @@ import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
import com.google.android.exoplayer2.source.DecryptableSampleQueueReader;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
......@@ -129,7 +128,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final Map<String, DrmInitData> overridingDrmInitData;
private SampleQueue[] sampleQueues;
private DecryptableSampleQueueReader[] sampleQueueReaders;
private int[] sampleQueueTrackIds;
private Set<Integer> sampleQueueMappingDoneByType;
private SparseIntArray sampleQueueIndicesByType;
......@@ -209,7 +207,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size());
sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size());
sampleQueues = new SampleQueue[0];
sampleQueueReaders = new DecryptableSampleQueueReader[0];
sampleQueueIsAudioVideoFlags = new boolean[0];
sampleQueuesEnabledStates = new boolean[0];
mediaChunks = new ArrayList<>();
......@@ -490,10 +487,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
// Discard as much as we can synchronously. We only do this if we're prepared, since otherwise
// sampleQueues may still be being modified by the loading thread.
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.discardToEnd();
}
for (DecryptableSampleQueueReader reader : sampleQueueReaders) {
reader.release();
sampleQueue.preRelease();
}
}
loader.release(this);
......@@ -504,9 +498,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Override
public void onLoaderReleased() {
resetSampleQueues();
for (DecryptableSampleQueueReader reader : sampleQueueReaders) {
reader.release();
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.release();
}
}
......@@ -521,12 +514,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
// SampleStream implementation.
public boolean isReady(int sampleQueueIndex) {
return !isPendingReset() && sampleQueueReaders[sampleQueueIndex].isReady(loadingFinished);
return !isPendingReset() && sampleQueues[sampleQueueIndex].isReady(loadingFinished);
}
public void maybeThrowError(int sampleQueueIndex) throws IOException {
maybeThrowError();
sampleQueueReaders[sampleQueueIndex].maybeThrowError();
sampleQueues[sampleQueueIndex].maybeThrowError();
}
public void maybeThrowError() throws IOException {
......@@ -559,7 +552,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
int result =
sampleQueueReaders[sampleQueueIndex].read(
sampleQueues[sampleQueueIndex].read(
formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs);
if (result == C.RESULT_FORMAT_READ) {
Format format = Assertions.checkNotNull(formatHolder.format);
......@@ -917,17 +910,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private SampleQueue createSampleQueue(int id, int type) {
int trackCount = sampleQueues.length;
SampleQueue trackOutput = new FormatAdjustingSampleQueue(allocator, overridingDrmInitData);
SampleQueue trackOutput =
new FormatAdjustingSampleQueue(allocator, drmSessionManager, overridingDrmInitData);
trackOutput.setSampleOffsetUs(sampleOffsetUs);
trackOutput.sourceId(chunkUid);
trackOutput.setUpstreamFormatChangeListener(this);
sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
sampleQueueTrackIds[trackCount] = id;
sampleQueues = Util.nullSafeArrayAppend(sampleQueues, trackOutput);
sampleQueueReaders =
Util.nullSafeArrayAppend(
sampleQueueReaders,
new DecryptableSampleQueueReader(sampleQueues[trackCount], drmSessionManager));
sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1);
sampleQueueIsAudioVideoFlags[trackCount] =
type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO;
......@@ -1295,8 +1285,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final Map<String, DrmInitData> overridingDrmInitData;
public FormatAdjustingSampleQueue(
Allocator allocator, Map<String, DrmInitData> overridingDrmInitData) {
super(allocator);
Allocator allocator,
DrmSessionManager<?> drmSessionManager,
Map<String, DrmInitData> overridingDrmInitData) {
super(allocator, drmSessionManager);
this.overridingDrmInitData = overridingDrmInitData;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment