Simplify bypass buffer batching

BatchBuffer has three different clear methods (clear, flush,
batchWasConsumed), and it's not hugely clear what each of them
does. In general, BatchBuffer owning the sample buffer seems
more complicated than having the caller own it, particularly
when it can be "pending" inside of the batch buffer.

This change moves ownership of the sample buffer to the
caller. BatchBuffer is simplified as a result. There are also
two behaviour changes:

1. The buffer's timeUs field is now set to the first sample's
   timestamp, rather than the last sample's.
2. A key-frame in the middle of the batch no longer causes the
   batch buffer to be considered a key-frame. Which seems like
   the right thing to do, because the batched data cannot be
   decoded independently of whatever came before it.

PiperOrigin-RevId: 350921306
This commit is contained in:
olly 2021-01-09 14:05:46 +00:00 committed by Ian Baker
parent 59aec416af
commit 7e295cbfc9
3 changed files with 313 additions and 341 deletions

View file

@ -15,172 +15,124 @@
*/ */
package com.google.android.exoplayer2.mediacodec; package com.google.android.exoplayer2.mediacodec;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** Buffer that stores multiple encoded access units to allow batch processing. */ /** Buffer to which multiple sample buffers can be appended for batch processing */
/* package */ final class BatchBuffer extends DecoderInputBuffer { /* 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; /** The default maximum number of samples that can be appended before the buffer is full. */
public static final int DEFAULT_MAX_SAMPLE_COUNT = 32;
/** /**
* Arbitrary limit to the memory used by a full batch buffer to avoid using too much memory for * The maximum size of the buffer in bytes. This prevents excessive memory usage for high bitrate
* very high bitrate. Equivalent of 75s of mp3 at highest bitrate (320kb/s) and 30s of AAC LC at * streams. The limit is equivalent of 75s of mp3 at highest bitrate (320kb/s) and 30s of AAC LC
* highest bitrate (800kb/s). That limit is ignored for the first access unit to avoid stalling * at highest bitrate (800kb/s). That limit is ignored for the first sample.
* stream with huge access units.
*/ */
private static final int BATCH_SIZE_BYTES = 3 * 1000 * 1024; @VisibleForTesting /* package */ static final int MAX_SIZE_BYTES = 3 * 1000 * 1024;
private final DecoderInputBuffer nextAccessUnitBuffer; private long lastSampleTimeUs;
private boolean hasPendingAccessUnit; private int sampleCount;
private int maxSampleCount;
private long firstAccessUnitTimeUs;
private int accessUnitCount;
private int maxAccessUnitCount;
public BatchBuffer() { public BatchBuffer() {
super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
nextAccessUnitBuffer = maxSampleCount = DEFAULT_MAX_SAMPLE_COUNT;
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 @Override
public void clear() { public void clear() {
flush(); super.clear();
maxAccessUnitCount = DEFAULT_BATCH_SIZE_ACCESS_UNITS; sampleCount = 0;
} }
/** Clear all access units from the BatchBuffer to empty it. */ /** Sets the maximum number of samples that can be appended before the buffer is full. */
public void flush() { public void setMaxSampleCount(@IntRange(from = 1) int maxSampleCount) {
clearMainBuffer(); checkArgument(maxSampleCount > 0);
nextAccessUnitBuffer.clear(); this.maxSampleCount = maxSampleCount;
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 * Returns the timestamp of the first sample in the buffer. The return value is undefined if
* #commitNextAccessUnit()}. * {@link #hasSamples()} is {@code false}.
*/ */
public DecoderInputBuffer getNextAccessUnitBuffer() { public long getFirstSampleTimeUs() {
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; return timeUs;
} }
/** Gets the number of access units contained in this batch buffer. */ /**
public int getAccessUnitCount() { * Returns the timestamp of the last sample in the buffer. The return value is undefined if {@link
return accessUnitCount; * #hasSamples()} is {@code false}.
*/
public long getLastSampleTimeUs() {
return lastSampleTimeUs;
} }
/** If the buffer contains no access units. */ /** Returns the number of samples in the buffer. */
public boolean isEmpty() { public int getSampleCount() {
return accessUnitCount == 0; return sampleCount;
} }
/** If more access units should be added to the batch buffer. */ /** Returns whether the buffer contains one or more samples. */
public boolean isFull() { public boolean hasSamples() {
return accessUnitCount >= maxAccessUnitCount return sampleCount > 0;
|| (data != null && data.position() >= BATCH_SIZE_BYTES)
|| hasPendingAccessUnit;
} }
/** /**
* Appends the staged access unit in this batch buffer. * Attempts to append the provided buffer.
* *
* @throws IllegalStateException If calling this method on a full or end of stream batch buffer. * @param buffer The buffer to try and append.
* @throws IllegalArgumentException If the {@code accessUnit} is encrypted or has * @return Whether the buffer was successfully appended.
* supplementalData, as batching of those state has not been implemented. * @throws IllegalArgumentException If the {@code buffer} is encrypted, has supplemental data, or
* is an end of stream buffer, none of which are supported.
*/ */
public void commitNextAccessUnit() { public boolean append(DecoderInputBuffer buffer) {
DecoderInputBuffer accessUnit = nextAccessUnitBuffer; checkArgument(!buffer.isEncrypted());
Assertions.checkState(!isFull() && !isEndOfStream()); checkArgument(!buffer.hasSupplementalData());
Assertions.checkArgument(!accessUnit.isEncrypted() && !accessUnit.hasSupplementalData()); checkArgument(!buffer.isEndOfStream());
if (!canBatch(accessUnit)) { if (!canAppendSampleBuffer(buffer)) {
hasPendingAccessUnit = true; // Delay the putAccessUnit until the batch buffer is empty. return false;
return;
} }
putAccessUnit(accessUnit); if (sampleCount++ == 0) {
} timeUs = buffer.timeUs;
if (buffer.isKeyFrame()) {
private boolean canBatch(DecoderInputBuffer accessUnit) { setFlags(C.BUFFER_FLAG_KEY_FRAME);
if (isEmpty()) { }
return true; // Batching with an empty batch must always succeed or the stream will stall.
} }
if (accessUnit.isDecodeOnly() != isDecodeOnly()) { if (buffer.isDecodeOnly()) {
return false; // Decode only and non decode only access units can not be batched together. setFlags(C.BUFFER_FLAG_DECODE_ONLY);
} }
@Nullable ByteBuffer bufferData = buffer.data;
@Nullable ByteBuffer accessUnitData = accessUnit.data; if (bufferData != null) {
if (accessUnitData != null ensureSpaceForWrite(bufferData.remaining());
&& this.data != null data.put(bufferData);
&& this.data.position() + accessUnitData.limit() >= BATCH_SIZE_BYTES) {
return false; // The batch buffer does not have the capacity to add this access unit.
} }
lastSampleTimeUs = buffer.timeUs;
return true; return true;
} }
private void putAccessUnit(DecoderInputBuffer accessUnit) { private boolean canAppendSampleBuffer(DecoderInputBuffer buffer) {
if (accessUnit.isEndOfStream()) { if (!hasSamples()) {
setFlags(C.BUFFER_FLAG_END_OF_STREAM); // Always allow appending when the buffer is empty, else no progress can be made.
} else { return true;
timeUs = accessUnit.timeUs;
if (accessUnit.isDecodeOnly()) {
setFlags(C.BUFFER_FLAG_DECODE_ONLY);
}
if (accessUnit.isKeyFrame()) {
setFlags(C.BUFFER_FLAG_KEY_FRAME);
}
@Nullable ByteBuffer accessUnitData = accessUnit.data;
if (accessUnitData != null) {
accessUnit.flip();
ensureSpaceForWrite(accessUnitData.remaining());
this.data.put(accessUnitData);
}
accessUnitCount++;
if (accessUnitCount == 1) {
firstAccessUnitTimeUs = timeUs;
}
} }
accessUnit.clear(); if (sampleCount >= maxSampleCount) {
} return false;
}
private void clearMainBuffer() { if (buffer.isDecodeOnly() != isDecodeOnly()) {
super.clear(); return false;
accessUnitCount = 0; }
firstAccessUnitTimeUs = C.TIME_UNSET; @Nullable ByteBuffer bufferData = buffer.data;
timeUs = C.TIME_UNSET; if (bufferData != null
&& data != null
&& data.position() + bufferData.remaining() > MAX_SIZE_BYTES) {
return false;
}
return true;
} }
} }

View file

@ -294,8 +294,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private final MediaCodecSelector mediaCodecSelector; private final MediaCodecSelector mediaCodecSelector;
private final boolean enableDecoderFallback; private final boolean enableDecoderFallback;
private final float assumedMinimumCodecOperatingRate; private final float assumedMinimumCodecOperatingRate;
private final DecoderInputBuffer buffer;
private final DecoderInputBuffer flagsOnlyBuffer; private final DecoderInputBuffer flagsOnlyBuffer;
private final DecoderInputBuffer buffer;
private final DecoderInputBuffer bypassSampleBuffer;
private final BatchBuffer bypassBatchBuffer; private final BatchBuffer bypassBatchBuffer;
private final TimedValueQueue<Format> formatQueue; private final TimedValueQueue<Format> formatQueue;
private final ArrayList<Long> decodeOnlyPresentationTimestamps; private final ArrayList<Long> decodeOnlyPresentationTimestamps;
@ -339,6 +340,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private boolean isDecodeOnlyOutputBuffer; private boolean isDecodeOnlyOutputBuffer;
private boolean isLastOutputBuffer; private boolean isLastOutputBuffer;
private boolean bypassEnabled; private boolean bypassEnabled;
private boolean bypassSampleBufferPending;
private boolean bypassDrainAndReinitialize; private boolean bypassDrainAndReinitialize;
private boolean codecReconfigured; private boolean codecReconfigured;
@ReconfigurationState private int codecReconfigurationState; @ReconfigurationState private int codecReconfigurationState;
@ -384,8 +386,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
this.mediaCodecSelector = checkNotNull(mediaCodecSelector); this.mediaCodecSelector = checkNotNull(mediaCodecSelector);
this.enableDecoderFallback = enableDecoderFallback; this.enableDecoderFallback = enableDecoderFallback;
this.assumedMinimumCodecOperatingRate = assumedMinimumCodecOperatingRate; this.assumedMinimumCodecOperatingRate = assumedMinimumCodecOperatingRate;
buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
bypassSampleBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
bypassBatchBuffer = new BatchBuffer();
formatQueue = new TimedValueQueue<>(); formatQueue = new TimedValueQueue<>();
decodeOnlyPresentationTimestamps = new ArrayList<>(); decodeOnlyPresentationTimestamps = new ArrayList<>();
outputBufferInfo = new MediaCodec.BufferInfo(); outputBufferInfo = new MediaCodec.BufferInfo();
@ -396,11 +400,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
outputStreamStartPositionUs = C.TIME_UNSET; outputStreamStartPositionUs = C.TIME_UNSET;
outputStreamOffsetUs = C.TIME_UNSET; outputStreamOffsetUs = C.TIME_UNSET;
bypassBatchBuffer = new BatchBuffer();
bypassBatchBuffer.ensureSpaceForWrite(/* length= */ 0);
// MediaCodec outputs audio buffers in native endian: // MediaCodec outputs audio buffers in native endian:
// https://developer.android.com/reference/android/media/MediaCodec#raw-audio-buffers // https://developer.android.com/reference/android/media/MediaCodec#raw-audio-buffers
// and code called from MediaCodecAudioRenderer.processOutputBuffer expects this endianness. // and code called from MediaCodecAudioRenderer.processOutputBuffer expects this endianness.
// Call ensureSpaceForWrite to make sure the buffer has non-null data, and set the expected
// endianness.
bypassBatchBuffer.ensureSpaceForWrite(/* length= */ 0);
bypassBatchBuffer.data.order(ByteOrder.nativeOrder()); bypassBatchBuffer.data.order(ByteOrder.nativeOrder());
resetCodecStateForRelease(); resetCodecStateForRelease();
} }
@ -688,7 +693,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
outputStreamEnded = false; outputStreamEnded = false;
pendingOutputEndOfStream = false; pendingOutputEndOfStream = false;
if (bypassEnabled) { if (bypassEnabled) {
bypassBatchBuffer.flush(); bypassBatchBuffer.clear();
bypassSampleBuffer.clear();
bypassSampleBufferPending = false;
} else { } else {
flushOrReinitializeCodec(); flushOrReinitializeCodec();
} }
@ -744,6 +751,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private void disableBypass() { private void disableBypass() {
bypassDrainAndReinitialize = false; bypassDrainAndReinitialize = false;
bypassBatchBuffer.clear(); bypassBatchBuffer.clear();
bypassSampleBuffer.clear();
bypassSampleBufferPending = false;
bypassEnabled = false; bypassEnabled = false;
} }
@ -1057,9 +1066,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
&& !MimeTypes.AUDIO_MPEG.equals(mimeType) && !MimeTypes.AUDIO_MPEG.equals(mimeType)
&& !MimeTypes.AUDIO_OPUS.equals(mimeType)) { && !MimeTypes.AUDIO_OPUS.equals(mimeType)) {
// TODO(b/154746451): Batching provokes frame drops in non offload. // TODO(b/154746451): Batching provokes frame drops in non offload.
bypassBatchBuffer.setMaxAccessUnitCount(1); bypassBatchBuffer.setMaxSampleCount(1);
} else { } else {
bypassBatchBuffer.setMaxAccessUnitCount(BatchBuffer.DEFAULT_BATCH_SIZE_ACCESS_UNITS); bypassBatchBuffer.setMaxSampleCount(BatchBuffer.DEFAULT_MAX_SAMPLE_COUNT);
} }
bypassEnabled = true; bypassEnabled = true;
} }
@ -2134,9 +2143,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private boolean bypassRender(long positionUs, long elapsedRealtimeUs) private boolean bypassRender(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException { throws ExoPlaybackException {
// Process any data in the batch buffer. // Process any batched data.
checkState(!outputStreamEnded); checkState(!outputStreamEnded);
if (!bypassBatchBuffer.isEmpty()) { if (bypassBatchBuffer.hasSamples()) {
if (processOutputBuffer( if (processOutputBuffer(
positionUs, positionUs,
elapsedRealtimeUs, elapsedRealtimeUs,
@ -2144,30 +2153,35 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
bypassBatchBuffer.data, bypassBatchBuffer.data,
outputIndex, outputIndex,
/* bufferFlags= */ 0, /* bufferFlags= */ 0,
bypassBatchBuffer.getAccessUnitCount(), bypassBatchBuffer.getSampleCount(),
bypassBatchBuffer.getFirstAccessUnitTimeUs(), bypassBatchBuffer.getFirstSampleTimeUs(),
bypassBatchBuffer.isDecodeOnly(), bypassBatchBuffer.isDecodeOnly(),
bypassBatchBuffer.isEndOfStream(), bypassBatchBuffer.isEndOfStream(),
outputFormat)) { outputFormat)) {
onProcessedOutputBuffer(bypassBatchBuffer.getLastAccessUnitTimeUs()); // The batch buffer has been fully processed.
onProcessedOutputBuffer(bypassBatchBuffer.getLastSampleTimeUs());
bypassBatchBuffer.clear();
} else { } else {
// Could not process the whole buffer. Try again later. // Could not process the whole batch buffer. Try again later.
return false; return false;
} }
} }
// Process end of stream, if reached. // Process end of stream, if reached.
if (bypassBatchBuffer.isEndOfStream()) { if (inputStreamEnded) {
outputStreamEnded = true; outputStreamEnded = true;
return false; return false;
} }
bypassBatchBuffer.batchWasConsumed(); if (bypassSampleBufferPending) {
Assertions.checkState(bypassBatchBuffer.append(bypassSampleBuffer));
bypassSampleBufferPending = false;
}
if (bypassDrainAndReinitialize) { if (bypassDrainAndReinitialize) {
if (!bypassBatchBuffer.isEmpty()) { if (bypassBatchBuffer.hasSamples()) {
// The bypassBatchBuffer.batchWasConsumed() call above caused a pending access unit to be // This can only happen if bypassSampleBufferPending was true above. Return true to try and
// made available inside the batch buffer, meaning it's once again non-empty. We need to // immediately process the sample, which has now been appended to the batch buffer.
// process this data before we can re-initialize.
return true; return true;
} }
// The new format might require using a codec rather than bypass. // The new format might require using a codec rather than bypass.
@ -2180,61 +2194,52 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
} }
// Fill the batch buffer for the next iteration. // Read from the input, appending any sample buffers to the batch buffer.
checkState(!inputStreamEnded); bypassRead();
FormatHolder formatHolder = getFormatHolder();
boolean formatChange = readBatchFromSource(formatHolder, bypassBatchBuffer);
if (!bypassBatchBuffer.isEmpty() && waitingForFirstSampleInFormat) { if (bypassBatchBuffer.hasSamples()) {
// This is the first buffer in a new format, the output format must be updated.
outputFormat = checkNotNull(inputFormat);
onOutputFormatChanged(outputFormat, /* mediaFormat= */ null);
waitingForFirstSampleInFormat = false;
}
if (formatChange) {
onInputFormatChanged(formatHolder);
}
boolean haveDataToProcess = false;
if (bypassBatchBuffer.isEndOfStream()) {
inputStreamEnded = true;
haveDataToProcess = true;
}
if (!bypassBatchBuffer.isEmpty()) {
bypassBatchBuffer.flip(); bypassBatchBuffer.flip();
haveDataToProcess = true;
} }
// We can make more progress if we have batched data or the EOS to process.
return haveDataToProcess; return bypassBatchBuffer.hasSamples() || inputStreamEnded;
} }
/** private void bypassRead() throws ExoPlaybackException {
* Fills the buffer with multiple access unit from the source. Has otherwise the same semantic as checkState(!inputStreamEnded);
* {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)}. Will stop early on format FormatHolder formatHolder = getFormatHolder();
* change, EOS or source starvation. bypassSampleBuffer.clear();
* while (true) {
* @return If the format has changed. bypassSampleBuffer.clear();
*/
private boolean readBatchFromSource(FormatHolder formatHolder, BatchBuffer batchBuffer) {
while (!batchBuffer.isFull() && !batchBuffer.isEndOfStream()) {
@SampleStream.ReadDataResult @SampleStream.ReadDataResult
int result = int result = readSource(formatHolder, bypassSampleBuffer, /* formatRequired= */ false);
readSource(
formatHolder, batchBuffer.getNextAccessUnitBuffer(), /* formatRequired= */ false);
switch (result) { switch (result) {
case C.RESULT_FORMAT_READ: case C.RESULT_FORMAT_READ:
return true; onInputFormatChanged(formatHolder);
break;
case C.RESULT_NOTHING_READ: case C.RESULT_NOTHING_READ:
return false; return;
case C.RESULT_BUFFER_READ: case C.RESULT_BUFFER_READ:
batchBuffer.commitNextAccessUnit(); if (bypassSampleBuffer.isEndOfStream()) {
inputStreamEnded = true;
return;
}
if (waitingForFirstSampleInFormat) {
// This is the first buffer in a new format, the output format must be updated.
outputFormat = checkNotNull(inputFormat);
onOutputFormatChanged(outputFormat, /* mediaFormat= */ null);
waitingForFirstSampleInFormat = false;
}
// Try to append the buffer to the batch buffer.
bypassSampleBuffer.flip();
if (!bypassBatchBuffer.append(bypassSampleBuffer)) {
bypassSampleBufferPending = true;
return;
}
break; break;
default: default:
throw new IllegalStateException(); // Unsupported result throw new IllegalStateException();
} }
} }
return false;
} }
private static boolean isMediaCodecException(IllegalStateException error) { private static boolean isMediaCodecException(IllegalStateException error) {

View file

@ -16,14 +16,14 @@
package com.google.android.exoplayer2.mediacodec; package com.google.android.exoplayer2.mediacodec;
import static com.google.android.exoplayer2.decoder.DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT;
import static com.google.android.exoplayer2.mediacodec.BatchBuffer.DEFAULT_MAX_SAMPLE_COUNT;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.common.primitives.Bytes;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -32,220 +32,235 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class BatchBufferTest { public final class BatchBufferTest {
/** Bigger than {@code BatchBuffer.BATCH_SIZE_BYTES} */ private final DecoderInputBuffer sampleBuffer =
private static final int BUFFER_SIZE_LARGER_THAN_BATCH_SIZE_BYTES = 6 * 1000 * 1024; new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DIRECT);
/** Smaller than {@code 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 final BatchBuffer batchBuffer = new BatchBuffer(); private final BatchBuffer batchBuffer = new BatchBuffer();
@Test @Test
public void newBatchBuffer_isEmpty() { public void newBatchBuffer_isEmpty() {
assertIsCleared(batchBuffer); assertThat(batchBuffer.getSampleCount()).isEqualTo(0);
assertThat(batchBuffer.hasSamples()).isFalse();
} }
@Test @Test
public void clear_empty_isEmpty() { public void appendSample() {
initSampleBuffer();
batchBuffer.append(sampleBuffer);
assertThat(batchBuffer.getSampleCount()).isEqualTo(1);
assertThat(batchBuffer.hasSamples()).isTrue();
}
@Test
public void appendSample_thenClear_isEmpty() {
initSampleBuffer();
batchBuffer.append(sampleBuffer);
batchBuffer.clear(); batchBuffer.clear();
assertIsCleared(batchBuffer); assertThat(batchBuffer.getSampleCount()).isEqualTo(0);
assertThat(batchBuffer.hasSamples()).isFalse();
} }
@Test @Test
public void clear_afterInsertingAccessUnit_isEmpty() { public void appendSample_updatesTimes() {
batchBuffer.commitNextAccessUnit(); initSampleBuffer(/* timeUs= */ 1234);
batchBuffer.append(sampleBuffer);
batchBuffer.clear(); initSampleBuffer(/* timeUs= */ 5678);
batchBuffer.append(sampleBuffer);
assertIsCleared(batchBuffer); assertThat(batchBuffer.timeUs).isEqualTo(1234);
assertThat(batchBuffer.getFirstSampleTimeUs()).isEqualTo(1234);
assertThat(batchBuffer.getLastSampleTimeUs()).isEqualTo(5678);
} }
@Test @Test
public void commitNextAccessUnit_addsAccessUnit() { public void appendSample_succeedsUntilDefaultMaxSampleCountReached_thenFails() {
batchBuffer.commitNextAccessUnit(); for (int i = 0; i < DEFAULT_MAX_SAMPLE_COUNT; i++) {
initSampleBuffer(/* timeUs= */ i);
assertThat(batchBuffer.append(sampleBuffer)).isTrue();
assertThat(batchBuffer.getSampleCount()).isEqualTo(i + 1);
}
assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1); initSampleBuffer(/* timeUs= */ DEFAULT_MAX_SAMPLE_COUNT);
assertThat(batchBuffer.append(sampleBuffer)).isFalse();
assertThat(batchBuffer.getSampleCount()).isEqualTo(DEFAULT_MAX_SAMPLE_COUNT);
assertThat(batchBuffer.getLastSampleTimeUs()).isEqualTo(DEFAULT_MAX_SAMPLE_COUNT - 1);
} }
@Test @Test
public void commitNextAccessUnit_untilFull_isFullAndNotEmpty() { public void appendSample_succeedsUntilCustomMaxSampleCountReached_thenFails() {
fillBatchBuffer(batchBuffer); int customMaxSampleCount = DEFAULT_MAX_SAMPLE_COUNT * 2;
batchBuffer.setMaxSampleCount(customMaxSampleCount);
for (int i = 0; i < customMaxSampleCount; i++) {
initSampleBuffer(/* timeUs= */ i);
assertThat(batchBuffer.append(sampleBuffer)).isTrue();
assertThat(batchBuffer.getSampleCount()).isEqualTo(i + 1);
}
assertThat(batchBuffer.isEmpty()).isFalse(); initSampleBuffer(/* timeUs= */ customMaxSampleCount);
assertThat(batchBuffer.isFull()).isTrue(); assertThat(batchBuffer.append(sampleBuffer)).isFalse();
assertThat(batchBuffer.getSampleCount()).isEqualTo(customMaxSampleCount);
assertThat(batchBuffer.getLastSampleTimeUs()).isEqualTo(customMaxSampleCount - 1);
} }
@Test @Test
public void commitNextAccessUnit_whenFull_throws() { public void appendFirstSample_withDecodeOnlyFlag_setsDecodeOnlyFlag() {
batchBuffer.setMaxAccessUnitCount(1); initSampleBuffer();
batchBuffer.commitNextAccessUnit(); sampleBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
batchBuffer.append(sampleBuffer);
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(); assertThat(batchBuffer.isDecodeOnly()).isTrue();
} }
@Test @Test
public void commitNextAccessUnit_whenAccessUnitIsEndOfStream_isEndOfSteam() { public void appendSecondSample_toDecodeOnlyBuffer_withDecodeOnlyFlag_succeeds() {
batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_END_OF_STREAM); initSampleBuffer();
sampleBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
batchBuffer.append(sampleBuffer);
batchBuffer.commitNextAccessUnit(); initSampleBuffer();
sampleBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
assertThat(batchBuffer.isEndOfStream()).isTrue(); assertThat(batchBuffer.append(sampleBuffer)).isTrue();
} }
@Test @Test
public void commitNextAccessUnit_whenAccessUnitIsKeyFrame_isKeyFrame() { public void appendSecondSample_toDecodeOnlyBuffer_withoutDecodeOnlyFlag_fails() {
batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_KEY_FRAME); initSampleBuffer();
sampleBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
batchBuffer.append(sampleBuffer);
batchBuffer.commitNextAccessUnit(); initSampleBuffer();
assertThat(batchBuffer.append(sampleBuffer)).isFalse();
}
@Test
public void appendSecondSample_toNonDecodeOnlyBuffer_withDecodeOnlyFlag_fails() {
initSampleBuffer();
batchBuffer.append(sampleBuffer);
initSampleBuffer();
sampleBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
assertThat(batchBuffer.append(sampleBuffer)).isFalse();
}
@Test
public void appendSecondSample_withKeyframeFlag_setsKeyframeFlag() {
initSampleBuffer();
sampleBuffer.setFlags(C.BUFFER_FLAG_KEY_FRAME);
batchBuffer.append(sampleBuffer);
assertThat(batchBuffer.isKeyFrame()).isTrue(); assertThat(batchBuffer.isKeyFrame()).isTrue();
} }
@Test @Test
public void commitNextAccessUnit_withData_dataIsCopiedInTheBatch() { public void appendSecondSample_withKeyframeFlag_doesNotSetKeyframeFlag() {
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length); initSampleBuffer();
batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT); batchBuffer.append(sampleBuffer);
batchBuffer.commitNextAccessUnit(); initSampleBuffer();
sampleBuffer.setFlags(C.BUFFER_FLAG_KEY_FRAME);
batchBuffer.append(sampleBuffer);
assertThat(batchBuffer.isKeyFrame()).isFalse();
}
@Test
public void appendSecondSample_doesNotClearKeyframeFlag() {
initSampleBuffer();
sampleBuffer.setFlags(C.BUFFER_FLAG_KEY_FRAME);
batchBuffer.append(sampleBuffer);
initSampleBuffer();
batchBuffer.append(sampleBuffer);
assertThat(batchBuffer.isKeyFrame()).isTrue();
}
@Test
public void appendSample_withEndOfStreamFlag_throws() {
sampleBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
assertThrows(IllegalArgumentException.class, () -> batchBuffer.append(sampleBuffer));
}
@Test
public void appendSample_withEncryptedFlag_throws() {
sampleBuffer.setFlags(C.BUFFER_FLAG_ENCRYPTED);
assertThrows(IllegalArgumentException.class, () -> batchBuffer.append(sampleBuffer));
}
@Test
public void appendSample_withSupplementalDataFlag_throws() {
sampleBuffer.setFlags(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA);
assertThrows(IllegalArgumentException.class, () -> batchBuffer.append(sampleBuffer));
}
@Test
public void appendTwoSamples_batchesData() {
initSampleBuffer(/* timeUs= */ 1234);
batchBuffer.append(sampleBuffer);
initSampleBuffer(/* timeUs= */ 5678);
batchBuffer.append(sampleBuffer);
batchBuffer.flip(); batchBuffer.flip();
assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1); ByteBuffer expected = ByteBuffer.allocate(Long.BYTES * 2);
assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(TEST_ACCESS_UNIT)); expected.putLong(1234);
expected.putLong(5678);
expected.flip();
assertThat(batchBuffer.data).isEqualTo(expected);
} }
@Test @Test
public void commitNextAccessUnit_nextAccessUnit_isClear() { public void appendFirstSample_exceedingMaxSize_succeeds() {
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length); sampleBuffer.ensureSpaceForWrite(BatchBuffer.MAX_SIZE_BYTES + 1);
batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT); sampleBuffer.data.position(BatchBuffer.MAX_SIZE_BYTES + 1);
batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_KEY_FRAME); sampleBuffer.flip();
assertThat(batchBuffer.append(sampleBuffer)).isTrue();
batchBuffer.commitNextAccessUnit();
DecoderInputBuffer nextAccessUnit = batchBuffer.getNextAccessUnitBuffer();
assertThat(nextAccessUnit.data).isNotNull();
assertThat(nextAccessUnit.data.position()).isEqualTo(0);
assertThat(nextAccessUnit.isKeyFrame()).isFalse();
} }
@Test @Test
public void commitNextAccessUnit_twice_bothAccessUnitAreConcatenated() { public void appendSecondSample_exceedingMaxSize_fails() {
// Commit TEST_ACCESS_UNIT initSampleBuffer();
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length); batchBuffer.append(sampleBuffer);
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(); int exceedsMaxSize = BatchBuffer.MAX_SIZE_BYTES - sampleBuffer.data.limit() + 1;
batchBuffer.flip(); sampleBuffer.clear();
sampleBuffer.ensureSpaceForWrite(exceedsMaxSize);
byte[] expected = Bytes.concat(TEST_ACCESS_UNIT, TEST_ACCESS_UNIT); sampleBuffer.data.position(exceedsMaxSize);
assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(expected)); sampleBuffer.flip();
assertThat(batchBuffer.append(sampleBuffer)).isFalse();
} }
@Test @Test
public void commitNextAccessUnit_whenAccessUnitIsHugeAndBatchBufferNotEmpty_isMarkedPending() { public void appendSecondSample_equalsMaxSize_succeeds() {
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length); initSampleBuffer();
batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT); batchBuffer.append(sampleBuffer);
batchBuffer.commitNextAccessUnit();
byte[] hugeAccessUnit = TestUtil.buildTestData(BUFFER_SIZE_LARGER_THAN_BATCH_SIZE_BYTES);
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(hugeAccessUnit.length);
batchBuffer.getNextAccessUnitBuffer().data.put(hugeAccessUnit);
batchBuffer.commitNextAccessUnit();
batchBuffer.batchWasConsumed(); int exceedsMaxSize = BatchBuffer.MAX_SIZE_BYTES - sampleBuffer.data.limit();
batchBuffer.flip(); sampleBuffer.clear();
sampleBuffer.ensureSpaceForWrite(exceedsMaxSize);
assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1); sampleBuffer.data.position(exceedsMaxSize);
assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(hugeAccessUnit)); sampleBuffer.flip();
assertThat(batchBuffer.append(sampleBuffer)).isTrue();
} }
@Test private void initSampleBuffer() {
public void batchWasConsumed_whenNotEmpty_isEmpty() { initSampleBuffer(/* timeUs= */ 0);
batchBuffer.commitNextAccessUnit();
batchBuffer.batchWasConsumed();
assertIsCleared(batchBuffer);
} }
@Test private void initSampleBuffer(long timeUs) {
public void batchWasConsumed_whenFull_isEmpty() { sampleBuffer.clear();
fillBatchBuffer(batchBuffer); sampleBuffer.timeUs = timeUs;
sampleBuffer.ensureSpaceForWrite(Long.BYTES);
batchBuffer.batchWasConsumed(); sampleBuffer.data.putLong(timeUs);
sampleBuffer.flip();
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();
}
} }