Commit 918963c2 by krocard Committed by Oliver Woodman

Do not use MediaCodec in passthrough mode.

Now that MediaCodec is not use in passthrough, no
MediaCodec should be created in this mode.

Additionally, do not instantiate a MediaCodec in passthrough

#exo-offload

PiperOrigin-RevId: 309916131
parent fa7d26dd
......@@ -55,9 +55,6 @@
* Add `DataSpec.Builder` and deprecate most `DataSpec` constructors.
* Add `DataSpec.customData` to allow applications to pass custom data
through `DataSource` chains.
* Add a sample count parameter to `MediaCodecRenderer.processOutputBuffer`
and `AudioSink.handleBuffer` to allow batching multiple encoded frames
in one buffer.
* Add a `Format.Builder` and deprecate all `Format.create*` methods and
most `Format.copyWith*` methods.
* Split `Format.bitrate` into `Format.averageBitrate` and
......@@ -133,6 +130,11 @@
directly instead.
* Update `CachedContentIndex` to use `SecureRandom` for generating the
initialization vector used to encrypt the cache contents.
* Audio:
* Add a sample count parameter to `MediaCodecRenderer.processOutputBuffer`
and `AudioSink.handleBuffer` to allow batching multiple encoded frames
in one buffer.
* No longer use a `MediaCodec` in audio passthrough mode.
* DASH:
* Merge trick play adaptation sets (i.e., adaptation sets marked with
`http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as
......
......@@ -217,7 +217,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@TunnelingSupport
int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
boolean supportsFormatDrm = supportsFormatDrm(format);
if (supportsFormatDrm && usePassthrough(format.channelCount, mimeType)) {
if (supportsFormatDrm
&& usePassthrough(format.channelCount, mimeType)
&& MediaCodecUtil.getPassthroughDecoderInfo() != null) {
return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport);
}
if ((MimeTypes.AUDIO_RAW.equals(mimeType)
......@@ -256,7 +258,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return Collections.emptyList();
}
if (usePassthrough(format.channelCount, mimeType)) {
return Collections.singletonList(MediaCodecUtil.getPassthroughDecoderInfo());
@Nullable MediaCodecInfo codecInfo = MediaCodecUtil.getPassthroughDecoderInfo();
if (codecInfo != null) {
return Collections.singletonList(codecInfo);
}
}
List<MediaCodecInfo> decoderInfos =
mediaCodecSelector.getDecoderInfos(
......@@ -273,19 +278,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return Collections.unmodifiableList(decoderInfos);
}
/**
* Returns whether encoded audio passthrough should be used for playing back the input format.
*
* @param channelCount The number of channels in the input media, or {@link Format#NO_VALUE} if
* not known.
* @param mimeType The type of input media.
* @return Whether passthrough playback is supported.
* @throws DecoderQueryException If there was an error querying the available passthrough
* decoders.
*/
protected boolean usePassthrough(int channelCount, String mimeType) throws DecoderQueryException {
return getPassthroughEncoding(channelCount, mimeType) != C.ENCODING_INVALID
&& MediaCodecUtil.getPassthroughDecoderInfo() != null;
@Override
protected boolean usePassthrough(int channelCount, String mimeType) {
return getPassthroughEncoding(channelCount, mimeType) != C.ENCODING_INVALID;
}
@Override
......@@ -423,20 +418,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} else {
channelMap = null;
}
configureAudioSink(encoding, channelCount, sampleRate, channelMap);
}
try {
audioSink.configure(
encoding,
channelCount,
sampleRate,
0,
channelMap,
inputFormat.encoderDelay,
inputFormat.encoderPadding);
} catch (AudioSink.ConfigurationException e) {
// TODO(internal: b/145658993) Use outputFormat instead.
throw createRendererException(e, inputFormat);
}
@Override
protected void onOutputPassthroughFormatChanged(Format outputFormat) throws ExoPlaybackException {
@C.Encoding
int encoding = getPassthroughEncoding(outputFormat.channelCount, outputFormat.sampleMimeType);
configureAudioSink(
encoding, outputFormat.channelCount, outputFormat.sampleRate, /* channelMap= */ null);
}
/**
......@@ -602,7 +592,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
protected boolean processOutputBuffer(
long positionUs,
long elapsedRealtimeUs,
MediaCodec codec,
@Nullable MediaCodec codec,
ByteBuffer buffer,
int bufferIndex,
int bufferFlags,
......@@ -612,7 +602,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
boolean isLastBuffer,
Format format)
throws ExoPlaybackException {
if (codecNeedsEosBufferTimestampWorkaround
if (codec != null
&& codecNeedsEosBufferTimestampWorkaround
&& bufferPresentationTimeUs == 0
&& (bufferFlags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
&& getLargestQueuedPresentationTimeUs() != C.TIME_UNSET) {
......@@ -626,7 +617,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
if (isDecodeOnlyBuffer) {
codec.releaseOutputBuffer(bufferIndex, false);
if (codec != null) {
codec.releaseOutputBuffer(bufferIndex, false);
}
decoderCounters.skippedOutputBufferCount++;
audioSink.handleDiscontinuity();
return true;
......@@ -641,7 +634,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
if (fullyConsumed) {
codec.releaseOutputBuffer(bufferIndex, false);
if (codec != null) {
codec.releaseOutputBuffer(bufferIndex, false);
}
decoderCounters.renderedOutputBufferCount++;
return true;
}
......@@ -769,6 +764,24 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return mediaFormat;
}
private void configureAudioSink(
int encoding, int channelCount, int sampleRate, @Nullable int[] channelMap)
throws ExoPlaybackException {
try {
audioSink.configure(
encoding,
channelCount,
sampleRate,
/* specifiedBufferSize= */ 0,
channelMap,
inputFormat.encoderDelay,
inputFormat.encoderPadding);
} catch (AudioSink.ConfigurationException e) {
// TODO(internal: b/145658993) Use outputFormat instead.
throw createRendererException(e, inputFormat);
}
}
private void updateCurrentPosition() {
long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded());
if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) {
......
/*
* Copyright (C) 2020 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.mediacodec;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import java.nio.ByteBuffer;
/** Buffer that stores multiple encoded access units to allow batch processing. */
/* package */ final class BatchBuffer extends DecoderInputBuffer {
/** Arbitrary limit to the number of access unit in a full batch buffer. */
public static final int DEFAULT_BATCH_SIZE_ACCESS_UNITS = 32;
/**
* Arbitrary limit to the memory used by a full batch buffer to avoid using too much memory for
* very high bitrate. Equivalent of 75s of mp3 at highest bitrate (320kb/s) and 30s of AAC LC at
* highest bitrate (800kb/s). That limit is ignored for the first access unit to avoid stalling
* stream with huge access units.
*/
private static final int BATCH_SIZE_BYTES = 3 * 1000 * 1024;
private final DecoderInputBuffer nextAccessUnitBuffer;
private boolean hasPendingAccessUnit;
private long firstAccessUnitTimeUs;
private int accessUnitCount;
private int maxAccessUnitCount;
public BatchBuffer() {
super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
nextAccessUnitBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
clear();
}
/** Sets the maximum number of access units the buffer can contain before being full. */
public void setMaxAccessUnitCount(@IntRange(from = 1) int maxAccessUnitCount) {
Assertions.checkArgument(maxAccessUnitCount > 0);
this.maxAccessUnitCount = maxAccessUnitCount;
}
/** Gets the maximum number of access units the buffer can contain before being full. */
public int getMaxAccessUnitCount() {
return maxAccessUnitCount;
}
/** Resets the state of this object to what it was after construction. */
@Override
public void clear() {
flush();
maxAccessUnitCount = DEFAULT_BATCH_SIZE_ACCESS_UNITS;
}
/** Clear all access units from the BatchBuffer to empty it. */
public void flush() {
clearMainBuffer();
nextAccessUnitBuffer.clear();
hasPendingAccessUnit = false;
}
/** Clears the state of the batch buffer to be ready to receive a new sequence of access units. */
public void batchWasConsumed() {
clearMainBuffer();
if (hasPendingAccessUnit) {
putAccessUnit(nextAccessUnitBuffer);
hasPendingAccessUnit = false;
}
}
/**
* Gets the buffer to fill-out that will then be append to the batch buffer with {@link
* #commitNextAccessUnit()}.
*/
public DecoderInputBuffer getNextAccessUnitBuffer() {
return nextAccessUnitBuffer;
}
/** Gets the timestamp of the first access unit in the buffer. */
public long getFirstAccessUnitTimeUs() {
return firstAccessUnitTimeUs;
}
/** Gets the timestamp of the last access unit in the buffer. */
public long getLastAccessUnitTimeUs() {
return timeUs;
}
/** Gets the number of access units contained in this batch buffer. */
public int getAccessUnitCount() {
return accessUnitCount;
}
/** If the buffer contains no access units. */
public boolean isEmpty() {
return accessUnitCount == 0;
}
/** If more access units should be added to the batch buffer. */
public boolean isFull() {
return accessUnitCount >= maxAccessUnitCount
|| (data != null && data.position() >= BATCH_SIZE_BYTES)
|| hasPendingAccessUnit;
}
/**
* Appends the staged access unit in this batch buffer.
*
* @throws IllegalStateException If calling this method on a full or end of stream batch buffer.
* @throws IllegalArgumentException If the {@code accessUnit} is encrypted or has
* supplementalData, as batching of those state has not been implemented.
*/
public void commitNextAccessUnit() {
DecoderInputBuffer accessUnit = nextAccessUnitBuffer;
Assertions.checkState(!isFull() && !isEndOfStream());
Assertions.checkArgument(!accessUnit.isEncrypted() && !accessUnit.hasSupplementalData());
if (!canBatch(accessUnit)) {
hasPendingAccessUnit = true; // Delay the putAccessUnit until the batch buffer is empty.
return;
}
putAccessUnit(accessUnit);
}
private boolean canBatch(DecoderInputBuffer accessUnit) {
if (isEmpty()) {
return true; // Batching with an empty batch must always succeed or the stream will stall.
}
if (accessUnit.isDecodeOnly() != isDecodeOnly()) {
return false; // Decode only and non decode only access units can not be batched together.
}
@Nullable ByteBuffer accessUnitData = accessUnit.data;
if (accessUnitData != null
&& this.data != null
&& this.data.position() + accessUnitData.limit() >= BATCH_SIZE_BYTES) {
return false; // The batch buffer does not have the capacity to add this access unit.
}
return true;
}
private void putAccessUnit(DecoderInputBuffer accessUnit) {
@Nullable ByteBuffer accessUnitData = accessUnit.data;
if (accessUnitData != null) {
accessUnit.flip();
ensureSpaceForWrite(accessUnitData.remaining());
this.data.put(accessUnitData);
}
if (accessUnit.isEndOfStream()) {
setFlags(C.BUFFER_FLAG_END_OF_STREAM);
}
if (accessUnit.isDecodeOnly()) {
setFlags(C.BUFFER_FLAG_DECODE_ONLY);
}
if (accessUnit.isKeyFrame()) {
setFlags(C.BUFFER_FLAG_KEY_FRAME);
}
accessUnitCount++;
timeUs = accessUnit.timeUs;
if (accessUnitCount == 1) { // First read of the buffer
firstAccessUnitTimeUs = timeUs;
}
accessUnit.clear();
}
private void clearMainBuffer() {
super.clear();
accessUnitCount = 0;
firstAccessUnitTimeUs = C.TIME_UNSET;
timeUs = C.TIME_UNSET;
}
}
......@@ -46,6 +46,7 @@ import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.TimedValueQueue;
import com.google.android.exoplayer2.util.TraceUtil;
......@@ -361,6 +362,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private final float assumedMinimumCodecOperatingRate;
private final DecoderInputBuffer buffer;
private final DecoderInputBuffer flagsOnlyBuffer;
private final BatchBuffer passthroughBatchBuffer;
private final TimedValueQueue<Format> formatQueue;
private final ArrayList<Long> decodeOnlyPresentationTimestamps;
private final MediaCodec.BufferInfo outputBufferInfo;
......@@ -401,6 +403,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private ByteBuffer outputBuffer;
private boolean isDecodeOnlyOutputBuffer;
private boolean isLastOutputBuffer;
private boolean passthroughEnabled;
private boolean passthroughDrainAndReinitialize;
private boolean codecReconfigured;
@ReconfigurationState private int codecReconfigurationState;
@DrainState private int codecDrainState;
......@@ -453,6 +457,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
outputStreamOffsetUs = C.TIME_UNSET;
passthroughBatchBuffer = new BatchBuffer();
resetCodecStateForRelease();
}
......@@ -567,9 +572,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@Nullable MediaCrypto crypto,
float codecOperatingRate);
protected final void maybeInitCodec() throws ExoPlaybackException {
if (codec != null || inputFormat == null) {
// We have a codec already, or we don't have a format with which to instantiate one.
protected final void maybeInitCodecOrPassthrough() throws ExoPlaybackException {
if (codec != null || passthroughEnabled || inputFormat == null) {
// We have a codec or using passthrough, or don't have a format to decide how to render.
return;
}
if (inputFormat.drmInitData == null
&& usePassthrough(inputFormat.channelCount, inputFormat.sampleMimeType)) {
initPassthrough(inputFormat);
return;
}
......@@ -618,6 +629,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
}
/**
* Returns whether encoded passthrough should be used for playing back the input format.
*
* @param channelCount The number of channels in the input media, or {@link Format#NO_VALUE} if
* not known.
* @param mimeType The type of input media.
* @return Whether passthrough playback is supported.
*/
protected boolean usePassthrough(int channelCount, String mimeType) {
return false;
}
protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {
return true;
}
......@@ -695,7 +718,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
inputStreamEnded = false;
outputStreamEnded = false;
pendingOutputEndOfStream = false;
flushOrReinitializeCodec();
if (passthroughEnabled) {
passthroughBatchBuffer.flush();
} else {
flushOrReinitializeCodec();
}
// If there is a format change on the input side still pending propagation to the output, we
// need to queue a format next time a buffer is read. This is because we may not read a new
// input format after the position reset.
......@@ -735,12 +762,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@Override
protected void onReset() {
try {
disablePassthrough();
releaseCodec();
} finally {
setSourceDrmSession(null);
}
}
private void disablePassthrough() {
passthroughDrainAndReinitialize = false;
passthroughBatchBuffer.clear();
passthroughEnabled = false;
}
protected void releaseCodec() {
try {
if (codecAdapter != null) {
......@@ -791,8 +825,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return;
}
// We have a format.
maybeInitCodec();
if (codec != null) {
maybeInitCodecOrPassthrough();
if (passthroughEnabled) {
TraceUtil.beginSection("renderPassthrough");
while (renderPassthrough(positionUs, elapsedRealtimeUs)) {}
TraceUtil.endSection();
} else if (codec != null) {
long renderStartTimeMs = SystemClock.elapsedRealtime();
TraceUtil.beginSection("drainAndFeed");
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)
......@@ -821,7 +859,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* This method is a no-op if the codec is {@code null}.
*
* <p>The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link
* #maybeInitCodec()} if the codec needs to be re-instantiated.
* #maybeInitCodecOrPassthrough()} if the codec needs to be re-instantiated.
*
* @return Whether the codec was released and reinitialized, rather than being flushed.
* @throws ExoPlaybackException If an error occurs re-instantiating the codec.
......@@ -829,7 +867,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
protected final boolean flushOrReinitializeCodec() throws ExoPlaybackException {
boolean released = flushOrReleaseCodec();
if (released) {
maybeInitCodec();
maybeInitCodecOrPassthrough();
}
return released;
}
......@@ -1022,6 +1060,26 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return codecInfos;
}
/**
* Configures passthrough where no codec is used. Called instead of {@link
* #configureCodec(MediaCodecInfo, MediaCodec, Format, MediaCrypto, float)} when no codec is used
* in passthrough.
*/
private void initPassthrough(Format format) {
disablePassthrough(); // In case of transition between 2 passthrough formats.
String mimeType = format.sampleMimeType;
if (!MimeTypes.AUDIO_AAC.equals(mimeType)
&& !MimeTypes.AUDIO_MPEG.equals(mimeType)
&& !MimeTypes.AUDIO_OPUS.equals(mimeType)) {
// TODO(b/154746451): Batching provokes frame drops in non offload passthrough.
passthroughBatchBuffer.setMaxAccessUnitCount(1);
} else {
passthroughBatchBuffer.setMaxAccessUnitCount(BatchBuffer.DEFAULT_BATCH_SIZE_ACCESS_UNITS);
}
passthroughEnabled = true;
}
private void initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto) throws Exception {
long codecInitializingTimestamp;
long codecInitializedTimestamp;
......@@ -1373,13 +1431,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
setSourceDrmSession(formatHolder.drmSession);
inputFormat = newFormat;
if (passthroughEnabled) {
passthroughDrainAndReinitialize = true;
return; // Need to drain passthrough first.
}
if (codec == null) {
maybeInitCodec();
maybeInitCodecOrPassthrough();
return;
}
// We have an existing codec that we may need to reconfigure or re-initialize. If the existing
// codec instance is being kept then its operating rate may need to be updated.
// We have an existing codec that we may need to reconfigure or re-initialize or release it to
// switch to passthrough. If the existing codec instance is being kept then its operating rate
// may need to be updated.
if ((sourceDrmSession == null && codecDrmSession != null)
|| (sourceDrmSession != null && codecDrmSession == null)
......@@ -1474,6 +1538,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
/**
* Called when the output {@link Format} changes in passthrough.
*
* <p>The default implementation is a no-op.
*
* @param outputFormat The new output {@link MediaFormat}.
* @throws ExoPlaybackException Thrown if an error occurs handling the new output media format.
*/
// TODO(b/154849417): merge with {@link #onOutputFormatChanged(Format)}.
protected void onOutputPassthroughFormatChanged(Format outputFormat) throws ExoPlaybackException {
// Do nothing.
}
/**
* Handles supplemental data associated with an input buffer.
*
* <p>The default implementation is a no-op.
......@@ -1821,7 +1898,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* iteration of the rendering loop.
* @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at the
* start of the current iteration of the rendering loop.
* @param codec The {@link MediaCodec} instance.
* @param codec The {@link MediaCodec} instance, or null in passthrough mode.
* @param buffer The output buffer to process.
* @param bufferIndex The index of the output buffer.
* @param bufferFlags The flags attached to the output buffer.
......@@ -1838,7 +1915,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
protected abstract boolean processOutputBuffer(
long positionUs,
long elapsedRealtimeUs,
MediaCodec codec,
@Nullable MediaCodec codec,
ByteBuffer buffer,
int bufferIndex,
int bufferFlags,
......@@ -1950,7 +2027,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private void reinitializeCodec() throws ExoPlaybackException {
releaseCodec();
maybeInitCodec();
maybeInitCodecOrPassthrough();
}
private boolean isDecodeOnlyBuffer(long presentationTimeUs) {
......@@ -2016,6 +2093,116 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return (FrameworkMediaCrypto) mediaCrypto;
}
/**
* Processes any pending batch of buffers without using a decoder, and drains a new batch of
* buffers from the source.
*
* @param positionUs The current media time in microseconds, measured at the start of the current
* iteration of the rendering loop.
* @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at the
* start of the current iteration of the rendering loop.
* @return If more buffers are ready to be rendered.
* @throws ExoPlaybackException If an error occurred while processing a buffer or handling a
* format change.
*/
private boolean renderPassthrough(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException {
BatchBuffer batchBuffer = passthroughBatchBuffer;
// Let's process the pending buffer if any.
Assertions.checkState(!outputStreamEnded);
if (!batchBuffer.isEmpty()) { // Optimisation: Do not process buffer if empty.
if (processOutputBuffer(
positionUs,
elapsedRealtimeUs,
/* codec= */ null,
batchBuffer.data,
outputIndex,
/* bufferFlags= */ 0,
batchBuffer.getAccessUnitCount(),
batchBuffer.getFirstAccessUnitTimeUs(),
batchBuffer.isDecodeOnly(),
batchBuffer.isEndOfStream(),
outputFormat)) {
// Buffer completely processed
onProcessedOutputBuffer(batchBuffer.getLastAccessUnitTimeUs());
} else {
return false; // Could not process buffer, let's try later.
}
}
if (batchBuffer.isEndOfStream()) {
outputStreamEnded = true;
return false;
}
batchBuffer.batchWasConsumed();
if (passthroughDrainAndReinitialize) {
if (!batchBuffer.isEmpty()) {
return true; // Drain the batch buffer before propagating the format change.
}
disablePassthrough(); // The new format might not be supported in passthrough.
passthroughDrainAndReinitialize = false;
maybeInitCodecOrPassthrough();
if (!passthroughEnabled) {
return false; // The new format is not supported in passthrough.
}
}
// Now refill the empty buffer for the next iteration.
Assertions.checkState(!inputStreamEnded);
FormatHolder formatHolder = getFormatHolder();
boolean formatChange = readBatchFromSource(formatHolder, batchBuffer);
if (!batchBuffer.isEmpty() && waitingForFirstSampleInFormat) {
// This is the first buffer in a new format, the output format must be updated.
outputFormat = Assertions.checkNotNull(inputFormat);
onOutputPassthroughFormatChanged(outputFormat);
waitingForFirstSampleInFormat = false;
}
if (formatChange) {
onInputFormatChanged(formatHolder);
}
if (batchBuffer.isEndOfStream()) {
inputStreamEnded = true;
}
if (batchBuffer.isEmpty()) {
return false; // The buffer could not be filled, there is nothing more to do.
}
batchBuffer.flip(); // Buffer at least partially full, it can now be processed.
return true;
}
/**
* Fills the buffer with multiple access unit from the source. Has otherwise the same semantic as
* {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)}. Will stop early on format
* change, EOS or source starvation.
*
* @return If the format has changed.
*/
private boolean readBatchFromSource(FormatHolder formatHolder, BatchBuffer batchBuffer) {
while (!batchBuffer.isFull() && !batchBuffer.isEndOfStream()) {
@SampleStream.ReadDataResult
int result =
readSource(
formatHolder, batchBuffer.getNextAccessUnitBuffer(), /* formatRequired= */ false);
switch (result) {
case C.RESULT_FORMAT_READ:
return true;
case C.RESULT_NOTHING_READ:
return false;
case C.RESULT_BUFFER_READ:
batchBuffer.commitNextAccessUnit();
break;
default:
throw new IllegalStateException(); // Unsupported result
}
}
return false;
}
private static boolean isMediaCodecException(IllegalStateException error) {
if (Util.SDK_INT >= 21 && isMediaCodecExceptionV21(error)) {
return true;
......
......@@ -514,7 +514,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
setOutputSurfaceV23(codec, surface);
} else {
releaseCodec();
maybeInitCodec();
maybeInitCodecOrPassthrough();
}
}
if (surface != null && surface != dummySurface) {
......@@ -753,7 +753,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
protected boolean processOutputBuffer(
long positionUs,
long elapsedRealtimeUs,
MediaCodec codec,
@Nullable MediaCodec codec,
ByteBuffer buffer,
int bufferIndex,
int bufferFlags,
......@@ -763,6 +763,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
boolean isLastBuffer,
Format format)
throws ExoPlaybackException {
Assertions.checkNotNull(codec); // Can not render video without codec
if (initialPositionUs == C.TIME_UNSET) {
initialPositionUs = positionUs;
}
......
/*
* Copyright (C) 2020 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.mediacodec;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.nio.ByteBuffer;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link BatchBuffer}. */
@RunWith(AndroidJUnit4.class)
public final class BatchBufferTest {
/** Bigger than {@link BatchBuffer#BATCH_SIZE_BYTES} */
private static final int BUFFER_SIZE_LARGER_THAN_BATCH_SIZE_BYTES = 100 * 1000 * 1000;
/** Smaller than {@link BatchBuffer#BATCH_SIZE_BYTES} */
private static final int BUFFER_SIZE_MUCH_SMALLER_THAN_BATCH_SIZE_BYTES = 100;
private static final byte[] TEST_ACCESS_UNIT =
TestUtil.buildTestData(BUFFER_SIZE_MUCH_SMALLER_THAN_BATCH_SIZE_BYTES);
private static final byte[] TEST_HUGE_ACCESS_UNIT =
TestUtil.buildTestData(BUFFER_SIZE_LARGER_THAN_BATCH_SIZE_BYTES);
private final BatchBuffer batchBuffer = new BatchBuffer();
@Test
public void newBatchBuffer_isEmpty() {
assertIsCleared(batchBuffer);
}
@Test
public void clear_empty_isEmpty() {
batchBuffer.clear();
assertIsCleared(batchBuffer);
}
@Test
public void clear_afterInsertingAccessUnit_isEmpty() {
batchBuffer.commitNextAccessUnit();
batchBuffer.clear();
assertIsCleared(batchBuffer);
}
@Test
public void commitNextAccessUnit_addsAccessUnit() {
batchBuffer.commitNextAccessUnit();
assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1);
}
@Test
public void commitNextAccessUnit_untilFull_isFullAndNotEmpty() {
fillBatchBuffer(batchBuffer);
assertThat(batchBuffer.isEmpty()).isFalse();
assertThat(batchBuffer.isFull()).isTrue();
}
@Test
public void commitNextAccessUnit_whenFull_throws() {
batchBuffer.setMaxAccessUnitCount(1);
batchBuffer.commitNextAccessUnit();
assertThrows(IllegalStateException.class, batchBuffer::commitNextAccessUnit);
}
@Test
public void commitNextAccessUnit_whenAccessUnitIsDecodeOnly_isDecodeOnly() {
batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_DECODE_ONLY);
batchBuffer.commitNextAccessUnit();
assertThat(batchBuffer.isDecodeOnly()).isTrue();
}
@Test
public void commitNextAccessUnit_whenAccessUnitIsEndOfStream_isEndOfSteam() {
batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_END_OF_STREAM);
batchBuffer.commitNextAccessUnit();
assertThat(batchBuffer.isEndOfStream()).isTrue();
}
@Test
public void commitNextAccessUnit_whenAccessUnitIsKeyFrame_isKeyFrame() {
batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_KEY_FRAME);
batchBuffer.commitNextAccessUnit();
assertThat(batchBuffer.isKeyFrame()).isTrue();
}
@Test
public void commitNextAccessUnit_withData_dataIsCopiedInTheBatch() {
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length);
batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT);
batchBuffer.commitNextAccessUnit();
batchBuffer.flip();
assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1);
assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(TEST_ACCESS_UNIT));
}
@Test
public void commitNextAccessUnit_nextAccessUnit_isClear() {
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length);
batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT);
batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_KEY_FRAME);
batchBuffer.commitNextAccessUnit();
DecoderInputBuffer nextAccessUnit = batchBuffer.getNextAccessUnitBuffer();
assertThat(nextAccessUnit.data).isNotNull();
assertThat(nextAccessUnit.data.position()).isEqualTo(0);
assertThat(nextAccessUnit.isKeyFrame()).isFalse();
}
@Test
public void commitNextAccessUnit_twice_bothAccessUnitAreConcatenated() {
// Commit TEST_ACCESS_UNIT
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length);
batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT);
batchBuffer.commitNextAccessUnit();
// Commit TEST_ACCESS_UNIT again
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length);
batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT);
batchBuffer.commitNextAccessUnit();
batchBuffer.flip();
byte[] expected = TestUtil.joinByteArrays(TEST_ACCESS_UNIT, TEST_ACCESS_UNIT);
assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(expected));
}
@Test
public void commitNextAccessUnit_whenAccessUnitIsHugeAndBatchBufferNotEmpty_isMarkedPending() {
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length);
batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT);
batchBuffer.commitNextAccessUnit();
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_HUGE_ACCESS_UNIT.length);
batchBuffer.getNextAccessUnitBuffer().data.put(TEST_HUGE_ACCESS_UNIT);
batchBuffer.commitNextAccessUnit();
batchBuffer.batchWasConsumed();
batchBuffer.flip();
assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1);
assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(TEST_HUGE_ACCESS_UNIT));
}
@Test
public void batchWasConsumed_whenNotEmpty_isEmpty() {
fillBatchBuffer(batchBuffer);
batchBuffer.batchWasConsumed();
assertIsCleared(batchBuffer);
}
@Test
public void batchWasConsumed_whenFull_isEmpty() {
fillBatchBuffer(batchBuffer);
batchBuffer.batchWasConsumed();
assertIsCleared(batchBuffer);
}
@Test
public void getMaxAccessUnitCount_whenSetToAPositiveValue_returnsIt() {
batchBuffer.setMaxAccessUnitCount(20);
assertThat(batchBuffer.getMaxAccessUnitCount()).isEqualTo(20);
}
@Test
public void setMaxAccessUnitCount_whenSetToNegative_throws() {
assertThrows(IllegalArgumentException.class, () -> batchBuffer.setMaxAccessUnitCount(-19));
}
@Test
public void setMaxAccessUnitCount_whenSetToZero_throws() {
assertThrows(IllegalArgumentException.class, () -> batchBuffer.setMaxAccessUnitCount(0));
}
@Test
public void setMaxAccessUnitCount_whenSetToTheNumberOfAccessUnitInTheBatch_isFull() {
batchBuffer.commitNextAccessUnit();
batchBuffer.setMaxAccessUnitCount(1);
assertThat(batchBuffer.isFull()).isTrue();
}
@Test
public void batchWasConsumed_whenAccessUnitIsPending_pendingAccessUnitIsInTheBatch() {
batchBuffer.commitNextAccessUnit();
batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_DECODE_ONLY);
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length);
batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT);
batchBuffer.commitNextAccessUnit();
batchBuffer.batchWasConsumed();
batchBuffer.flip();
assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1);
assertThat(batchBuffer.isDecodeOnly()).isTrue();
assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(TEST_ACCESS_UNIT));
}
private static void fillBatchBuffer(BatchBuffer batchBuffer) {
int maxAccessUnit = batchBuffer.getMaxAccessUnitCount();
while (!batchBuffer.isFull()) {
assertThat(maxAccessUnit--).isNotEqualTo(0);
batchBuffer.commitNextAccessUnit();
}
}
private static void assertIsCleared(BatchBuffer batchBuffer) {
assertThat(batchBuffer.getFirstAccessUnitTimeUs()).isEqualTo(C.TIME_UNSET);
assertThat(batchBuffer.getLastAccessUnitTimeUs()).isEqualTo(C.TIME_UNSET);
assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(0);
assertThat(batchBuffer.isEmpty()).isTrue();
assertThat(batchBuffer.isFull()).isFalse();
}
}
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