diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/BatchBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/BatchBuffer.java index f770e92a21..a34a3b4f34 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/BatchBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/BatchBuffer.java @@ -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; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 45d9b53d90..61e121d6b7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -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 formatQueue; private final ArrayList 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) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java index 6579e8ee06..b488403a68 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java @@ -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(); - } }