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;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
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. */
/** Buffer to which multiple sample buffers can be appended for 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;
/** 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
* 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.
* The maximum size of the buffer in bytes. This prevents excessive memory usage for high bitrate
* streams. The limit is 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 sample.
*/
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 boolean hasPendingAccessUnit;
private long firstAccessUnitTimeUs;
private int accessUnitCount;
private int maxAccessUnitCount;
private long lastSampleTimeUs;
private int sampleCount;
private int maxSampleCount;
public BatchBuffer() {
super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
nextAccessUnitBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
clear();
maxSampleCount = DEFAULT_MAX_SAMPLE_COUNT;
}
/** 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;
super.clear();
sampleCount = 0;
}
/** 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;
}
/** Sets the maximum number of samples that can be appended before the buffer is full. */
public void setMaxSampleCount(@IntRange(from = 1) int maxSampleCount) {
checkArgument(maxSampleCount > 0);
this.maxSampleCount = maxSampleCount;
}
/**
* Gets the buffer to fill-out that will then be append to the batch buffer with {@link
* #commitNextAccessUnit()}.
* Returns the timestamp of the first sample in the buffer. The return value is undefined if
* {@link #hasSamples()} is {@code false}.
*/
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() {
public long getFirstSampleTimeUs() {
return timeUs;
}
/** Gets the number of access units contained in this batch buffer. */
public int getAccessUnitCount() {
return accessUnitCount;
/**
* Returns the timestamp of the last sample in the buffer. The return value is undefined if {@link
* #hasSamples()} is {@code false}.
*/
public long getLastSampleTimeUs() {
return lastSampleTimeUs;
}
/** If the buffer contains no access units. */
public boolean isEmpty() {
return accessUnitCount == 0;
/** Returns the number of samples in the buffer. */
public int getSampleCount() {
return sampleCount;
}
/** 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;
/** Returns whether the buffer contains one or more samples. */
public boolean hasSamples() {
return sampleCount > 0;
}
/**
* 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.
* @throws IllegalArgumentException If the {@code accessUnit} is encrypted or has
* supplementalData, as batching of those state has not been implemented.
* @param buffer The buffer to try and append.
* @return Whether the buffer was successfully appended.
* @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() {
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;
public boolean append(DecoderInputBuffer buffer) {
checkArgument(!buffer.isEncrypted());
checkArgument(!buffer.hasSupplementalData());
checkArgument(!buffer.isEndOfStream());
if (!canAppendSampleBuffer(buffer)) {
return false;
}
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 (sampleCount++ == 0) {
timeUs = buffer.timeUs;
if (buffer.isKeyFrame()) {
setFlags(C.BUFFER_FLAG_KEY_FRAME);
}
}
if (accessUnit.isDecodeOnly() != isDecodeOnly()) {
return false; // Decode only and non decode only access units can not be batched together.
if (buffer.isDecodeOnly()) {
setFlags(C.BUFFER_FLAG_DECODE_ONLY);
}
@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.
@Nullable ByteBuffer bufferData = buffer.data;
if (bufferData != null) {
ensureSpaceForWrite(bufferData.remaining());
data.put(bufferData);
}
lastSampleTimeUs = buffer.timeUs;
return true;
}
private void putAccessUnit(DecoderInputBuffer accessUnit) {
if (accessUnit.isEndOfStream()) {
setFlags(C.BUFFER_FLAG_END_OF_STREAM);
} else {
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;
}
private boolean canAppendSampleBuffer(DecoderInputBuffer buffer) {
if (!hasSamples()) {
// Always allow appending when the buffer is empty, else no progress can be made.
return true;
}
accessUnit.clear();
}
private void clearMainBuffer() {
super.clear();
accessUnitCount = 0;
firstAccessUnitTimeUs = C.TIME_UNSET;
timeUs = C.TIME_UNSET;
if (sampleCount >= maxSampleCount) {
return false;
}
if (buffer.isDecodeOnly() != isDecodeOnly()) {
return false;
}
@Nullable ByteBuffer bufferData = buffer.data;
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 boolean enableDecoderFallback;
private final float assumedMinimumCodecOperatingRate;
private final DecoderInputBuffer buffer;
private final DecoderInputBuffer flagsOnlyBuffer;
private final DecoderInputBuffer buffer;
private final DecoderInputBuffer bypassSampleBuffer;
private final BatchBuffer bypassBatchBuffer;
private final TimedValueQueue<Format> formatQueue;
private final ArrayList<Long> decodeOnlyPresentationTimestamps;
@ -339,6 +340,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private boolean isDecodeOnlyOutputBuffer;
private boolean isLastOutputBuffer;
private boolean bypassEnabled;
private boolean bypassSampleBufferPending;
private boolean bypassDrainAndReinitialize;
private boolean codecReconfigured;
@ReconfigurationState private int codecReconfigurationState;
@ -384,8 +386,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
this.mediaCodecSelector = checkNotNull(mediaCodecSelector);
this.enableDecoderFallback = enableDecoderFallback;
this.assumedMinimumCodecOperatingRate = assumedMinimumCodecOperatingRate;
buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
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<>();
decodeOnlyPresentationTimestamps = new ArrayList<>();
outputBufferInfo = new MediaCodec.BufferInfo();
@ -396,11 +400,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
outputStreamStartPositionUs = C.TIME_UNSET;
outputStreamOffsetUs = C.TIME_UNSET;
bypassBatchBuffer = new BatchBuffer();
bypassBatchBuffer.ensureSpaceForWrite(/* length= */ 0);
// MediaCodec outputs audio buffers in native endian:
// https://developer.android.com/reference/android/media/MediaCodec#raw-audio-buffers
// 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());
resetCodecStateForRelease();
}
@ -688,7 +693,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
outputStreamEnded = false;
pendingOutputEndOfStream = false;
if (bypassEnabled) {
bypassBatchBuffer.flush();
bypassBatchBuffer.clear();
bypassSampleBuffer.clear();
bypassSampleBufferPending = false;
} else {
flushOrReinitializeCodec();
}
@ -744,6 +751,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private void disableBypass() {
bypassDrainAndReinitialize = false;
bypassBatchBuffer.clear();
bypassSampleBuffer.clear();
bypassSampleBufferPending = false;
bypassEnabled = false;
}
@ -1057,9 +1066,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
&& !MimeTypes.AUDIO_MPEG.equals(mimeType)
&& !MimeTypes.AUDIO_OPUS.equals(mimeType)) {
// TODO(b/154746451): Batching provokes frame drops in non offload.
bypassBatchBuffer.setMaxAccessUnitCount(1);
bypassBatchBuffer.setMaxSampleCount(1);
} else {
bypassBatchBuffer.setMaxAccessUnitCount(BatchBuffer.DEFAULT_BATCH_SIZE_ACCESS_UNITS);
bypassBatchBuffer.setMaxSampleCount(BatchBuffer.DEFAULT_MAX_SAMPLE_COUNT);
}
bypassEnabled = true;
}
@ -2134,9 +2143,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private boolean bypassRender(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException {
// Process any data in the batch buffer.
// Process any batched data.
checkState(!outputStreamEnded);
if (!bypassBatchBuffer.isEmpty()) {
if (bypassBatchBuffer.hasSamples()) {
if (processOutputBuffer(
positionUs,
elapsedRealtimeUs,
@ -2144,30 +2153,35 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
bypassBatchBuffer.data,
outputIndex,
/* bufferFlags= */ 0,
bypassBatchBuffer.getAccessUnitCount(),
bypassBatchBuffer.getFirstAccessUnitTimeUs(),
bypassBatchBuffer.getSampleCount(),
bypassBatchBuffer.getFirstSampleTimeUs(),
bypassBatchBuffer.isDecodeOnly(),
bypassBatchBuffer.isEndOfStream(),
outputFormat)) {
onProcessedOutputBuffer(bypassBatchBuffer.getLastAccessUnitTimeUs());
// The batch buffer has been fully processed.
onProcessedOutputBuffer(bypassBatchBuffer.getLastSampleTimeUs());
bypassBatchBuffer.clear();
} else {
// Could not process the whole buffer. Try again later.
// Could not process the whole batch buffer. Try again later.
return false;
}
}
// Process end of stream, if reached.
if (bypassBatchBuffer.isEndOfStream()) {
if (inputStreamEnded) {
outputStreamEnded = true;
return false;
}
bypassBatchBuffer.batchWasConsumed();
if (bypassSampleBufferPending) {
Assertions.checkState(bypassBatchBuffer.append(bypassSampleBuffer));
bypassSampleBufferPending = false;
}
if (bypassDrainAndReinitialize) {
if (!bypassBatchBuffer.isEmpty()) {
// The bypassBatchBuffer.batchWasConsumed() call above caused a pending access unit to be
// made available inside the batch buffer, meaning it's once again non-empty. We need to
// process this data before we can re-initialize.
if (bypassBatchBuffer.hasSamples()) {
// This can only happen if bypassSampleBufferPending was true above. Return true to try and
// immediately process the sample, which has now been appended to the batch buffer.
return true;
}
// 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.
checkState(!inputStreamEnded);
FormatHolder formatHolder = getFormatHolder();
boolean formatChange = readBatchFromSource(formatHolder, bypassBatchBuffer);
// Read from the input, appending any sample buffers to the batch buffer.
bypassRead();
if (!bypassBatchBuffer.isEmpty() && 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;
}
if (formatChange) {
onInputFormatChanged(formatHolder);
}
boolean haveDataToProcess = false;
if (bypassBatchBuffer.isEndOfStream()) {
inputStreamEnded = true;
haveDataToProcess = true;
}
if (!bypassBatchBuffer.isEmpty()) {
if (bypassBatchBuffer.hasSamples()) {
bypassBatchBuffer.flip();
haveDataToProcess = true;
}
return haveDataToProcess;
// We can make more progress if we have batched data or the EOS to process.
return bypassBatchBuffer.hasSamples() || inputStreamEnded;
}
/**
* 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()) {
private void bypassRead() throws ExoPlaybackException {
checkState(!inputStreamEnded);
FormatHolder formatHolder = getFormatHolder();
bypassSampleBuffer.clear();
while (true) {
bypassSampleBuffer.clear();
@SampleStream.ReadDataResult
int result =
readSource(
formatHolder, batchBuffer.getNextAccessUnitBuffer(), /* formatRequired= */ false);
int result = readSource(formatHolder, bypassSampleBuffer, /* formatRequired= */ false);
switch (result) {
case C.RESULT_FORMAT_READ:
return true;
onInputFormatChanged(formatHolder);
break;
case C.RESULT_NOTHING_READ:
return false;
return;
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;
default:
throw new IllegalStateException(); // Unsupported result
throw new IllegalStateException();
}
}
return false;
}
private static boolean isMediaCodecException(IllegalStateException error) {

View file

@ -16,14 +16,14 @@
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 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 com.google.common.primitives.Bytes;
import java.nio.ByteBuffer;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -32,220 +32,235 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public final class BatchBufferTest {
/** Bigger than {@code BatchBuffer.BATCH_SIZE_BYTES} */
private static final int BUFFER_SIZE_LARGER_THAN_BATCH_SIZE_BYTES = 6 * 1000 * 1024;
/** 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 DecoderInputBuffer sampleBuffer =
new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DIRECT);
private final BatchBuffer batchBuffer = new BatchBuffer();
@Test
public void newBatchBuffer_isEmpty() {
assertIsCleared(batchBuffer);
assertThat(batchBuffer.getSampleCount()).isEqualTo(0);
assertThat(batchBuffer.hasSamples()).isFalse();
}
@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();
assertIsCleared(batchBuffer);
assertThat(batchBuffer.getSampleCount()).isEqualTo(0);
assertThat(batchBuffer.hasSamples()).isFalse();
}
@Test
public void clear_afterInsertingAccessUnit_isEmpty() {
batchBuffer.commitNextAccessUnit();
public void appendSample_updatesTimes() {
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
public void commitNextAccessUnit_addsAccessUnit() {
batchBuffer.commitNextAccessUnit();
public void appendSample_succeedsUntilDefaultMaxSampleCountReached_thenFails() {
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
public void commitNextAccessUnit_untilFull_isFullAndNotEmpty() {
fillBatchBuffer(batchBuffer);
public void appendSample_succeedsUntilCustomMaxSampleCountReached_thenFails() {
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();
assertThat(batchBuffer.isFull()).isTrue();
initSampleBuffer(/* timeUs= */ customMaxSampleCount);
assertThat(batchBuffer.append(sampleBuffer)).isFalse();
assertThat(batchBuffer.getSampleCount()).isEqualTo(customMaxSampleCount);
assertThat(batchBuffer.getLastSampleTimeUs()).isEqualTo(customMaxSampleCount - 1);
}
@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();
public void appendFirstSample_withDecodeOnlyFlag_setsDecodeOnlyFlag() {
initSampleBuffer();
sampleBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
batchBuffer.append(sampleBuffer);
assertThat(batchBuffer.isDecodeOnly()).isTrue();
}
@Test
public void commitNextAccessUnit_whenAccessUnitIsEndOfStream_isEndOfSteam() {
batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_END_OF_STREAM);
public void appendSecondSample_toDecodeOnlyBuffer_withDecodeOnlyFlag_succeeds() {
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
public void commitNextAccessUnit_whenAccessUnitIsKeyFrame_isKeyFrame() {
batchBuffer.getNextAccessUnitBuffer().setFlags(C.BUFFER_FLAG_KEY_FRAME);
public void appendSecondSample_toDecodeOnlyBuffer_withoutDecodeOnlyFlag_fails() {
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();
}
@Test
public void commitNextAccessUnit_withData_dataIsCopiedInTheBatch() {
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length);
batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT);
public void appendSecondSample_withKeyframeFlag_doesNotSetKeyframeFlag() {
initSampleBuffer();
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();
assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1);
assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(TEST_ACCESS_UNIT));
ByteBuffer expected = ByteBuffer.allocate(Long.BYTES * 2);
expected.putLong(1234);
expected.putLong(5678);
expected.flip();
assertThat(batchBuffer.data).isEqualTo(expected);
}
@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();
public void appendFirstSample_exceedingMaxSize_succeeds() {
sampleBuffer.ensureSpaceForWrite(BatchBuffer.MAX_SIZE_BYTES + 1);
sampleBuffer.data.position(BatchBuffer.MAX_SIZE_BYTES + 1);
sampleBuffer.flip();
assertThat(batchBuffer.append(sampleBuffer)).isTrue();
}
@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);
public void appendSecondSample_exceedingMaxSize_fails() {
initSampleBuffer();
batchBuffer.append(sampleBuffer);
batchBuffer.commitNextAccessUnit();
batchBuffer.flip();
byte[] expected = Bytes.concat(TEST_ACCESS_UNIT, TEST_ACCESS_UNIT);
assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(expected));
int exceedsMaxSize = BatchBuffer.MAX_SIZE_BYTES - sampleBuffer.data.limit() + 1;
sampleBuffer.clear();
sampleBuffer.ensureSpaceForWrite(exceedsMaxSize);
sampleBuffer.data.position(exceedsMaxSize);
sampleBuffer.flip();
assertThat(batchBuffer.append(sampleBuffer)).isFalse();
}
@Test
public void commitNextAccessUnit_whenAccessUnitIsHugeAndBatchBufferNotEmpty_isMarkedPending() {
batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length);
batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT);
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();
public void appendSecondSample_equalsMaxSize_succeeds() {
initSampleBuffer();
batchBuffer.append(sampleBuffer);
batchBuffer.batchWasConsumed();
batchBuffer.flip();
assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1);
assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(hugeAccessUnit));
int exceedsMaxSize = BatchBuffer.MAX_SIZE_BYTES - sampleBuffer.data.limit();
sampleBuffer.clear();
sampleBuffer.ensureSpaceForWrite(exceedsMaxSize);
sampleBuffer.data.position(exceedsMaxSize);
sampleBuffer.flip();
assertThat(batchBuffer.append(sampleBuffer)).isTrue();
}
@Test
public void batchWasConsumed_whenNotEmpty_isEmpty() {
batchBuffer.commitNextAccessUnit();
batchBuffer.batchWasConsumed();
assertIsCleared(batchBuffer);
private void initSampleBuffer() {
initSampleBuffer(/* timeUs= */ 0);
}
@Test
public void batchWasConsumed_whenFull_isEmpty() {
fillBatchBuffer(batchBuffer);
batchBuffer.batchWasConsumed();
assertIsCleared(batchBuffer);
private void initSampleBuffer(long timeUs) {
sampleBuffer.clear();
sampleBuffer.timeUs = timeUs;
sampleBuffer.ensureSpaceForWrite(Long.BYTES);
sampleBuffer.data.putLong(timeUs);
sampleBuffer.flip();
}
@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();
}
}