diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0e1cb2111e..0fd43b20ea 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -75,6 +75,8 @@ in offload, then no track will be selected. * Disabling gapless support for offload when pre-API level 33 due to playback position issue after track transition. + * Prepend Ogg ID Header and Comment Header Pages to bitstream for + offloaded Opus playback in accordance with RFC 7845. * Remove parameter `enableOffload` from `DefaultRenderersFactory.buildAudioSink` method signature. * Remove method `DefaultAudioSink.Builder.setOffloadMode`. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/OggOpusAudioPacketizer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/OggOpusAudioPacketizer.java index 2b8917d1ae..c2315edfed 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/OggOpusAudioPacketizer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/OggOpusAudioPacketizer.java @@ -18,19 +18,37 @@ package androidx.media3.exoplayer.audio; import static androidx.media3.common.audio.AudioProcessor.EMPTY_BUFFER; import static androidx.media3.common.util.Assertions.checkNotNull; +import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.extractor.OpusUtil; +import com.google.common.primitives.UnsignedBytes; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.List; -/** A packetizer that encapsulates OPUS audio encodings in OGG packets. */ +/** A packetizer that encapsulates Opus audio encodings in Ogg packets. */ @UnstableApi public final class OggOpusAudioPacketizer { + private static final int CHECKSUM_INDEX = 22; + /** ID Header and Comment Header pages are 0 and 1 respectively */ - private static final int FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE = 2; + private static final int FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER = 2; + + private static final int OGG_PACKET_HEADER_LENGTH = 28; + private static final int SERIAL_NUMBER = 0; + private static final byte[] OGG_DEFAULT_ID_HEADER_PAGE = + new byte[] { + 79, 103, 103, 83, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, -43, -59, -9, 1, + 19, 79, 112, 117, 115, 72, 101, 97, 100, 1, 2, 56, 1, -128, -69, 0, 0, 0, 0, 0 + }; + private static final byte[] OGG_DEFAULT_COMMENT_HEADER_PAGE = + new byte[] { + 79, 103, 103, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 11, -103, 87, 83, 1, + 16, 79, 112, 117, 115, 84, 97, 103, 115, 0, 0, 0, 0, 0, 0, 0, 0 + }; private ByteBuffer outputBuffer; private int pageSequenceNumber; @@ -40,7 +58,7 @@ public final class OggOpusAudioPacketizer { public OggOpusAudioPacketizer() { outputBuffer = EMPTY_BUFFER; granulePosition = 0; - pageSequenceNumber = FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE; + pageSequenceNumber = FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER; } /** @@ -49,13 +67,23 @@ public final class OggOpusAudioPacketizer { * @param inputBuffer The input buffer to packetize. It must be a direct {@link ByteBuffer} with * LITTLE_ENDIAN order. The contents will be overwritten with the Ogg packet. The caller * retains ownership of the provided buffer. + * @param initializationData contains set-up data for the Opus Decoder. The data will be provided + * in an Ogg ID Header Page prepended to the bitstream. The list should contain either one or + * three byte arrays. The first item is the payload for the Ogg ID Header Page. If three + * items, then it also contains the Opus pre-skip and seek pre-roll values in that order. */ - public void packetize(DecoderInputBuffer inputBuffer) { + public void packetize(DecoderInputBuffer inputBuffer, List initializationData) { checkNotNull(inputBuffer.data); if (inputBuffer.data.limit() - inputBuffer.data.position() == 0) { return; } - outputBuffer = packetizeInternal(inputBuffer.data); + @Nullable + byte[] providedOggIdHeaderPayloadBytes = + pageSequenceNumber == FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER + && (initializationData.size() == 1 || initializationData.size() == 3) + ? initializationData.get(0) + : null; + outputBuffer = packetizeInternal(inputBuffer.data, providedOggIdHeaderPayloadBytes); inputBuffer.clear(); inputBuffer.ensureSpaceForWrite(outputBuffer.remaining()); inputBuffer.data.put(outputBuffer); @@ -66,16 +94,24 @@ public final class OggOpusAudioPacketizer { public void reset() { outputBuffer = EMPTY_BUFFER; granulePosition = 0; - pageSequenceNumber = FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE; + pageSequenceNumber = FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER; } /** * Fill outputBuffer with an Ogg packet encapsulating the inputBuffer. * - * @param inputBuffer contains Opus to wrap in Ogg packet + *

If {@code providedOggIdHeaderPayloadBytes} is {@code null} and {@link #pageSequenceNumber} + * is {@link #FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER}, then {@link #OGG_DEFAULT_ID_HEADER_PAGE} + * will be prepended to the Ogg Opus Audio packets for the Ogg ID Header Page. + * + * @param inputBuffer contains Opus to wrap in Ogg packet. + * @param providedOggIdHeaderPayloadBytes containing the Ogg ID Header Page payload. Expected to + * be {@code null} if {@link #pageSequenceNumber} is not {@link + * #FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER}. * @return {@link ByteBuffer} containing Ogg packet */ - private ByteBuffer packetizeInternal(ByteBuffer inputBuffer) { + private ByteBuffer packetizeInternal( + ByteBuffer inputBuffer, @Nullable byte[] providedOggIdHeaderPayloadBytes) { int position = inputBuffer.position(); int limit = inputBuffer.limit(); int inputBufferSize = limit - position; @@ -86,38 +122,37 @@ public final class OggOpusAudioPacketizer { int outputPacketSize = headerSize + inputBufferSize; + // If first audio sample in stream, then the packetizer will add Ogg ID Header and Comment + // Header Pages. Include additional page lengths in buffer size calculation. + int oggIdHeaderPageSize = 0; + if (pageSequenceNumber == FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER) { + oggIdHeaderPageSize = + providedOggIdHeaderPayloadBytes != null + ? OGG_PACKET_HEADER_LENGTH + providedOggIdHeaderPayloadBytes.length + : OGG_DEFAULT_ID_HEADER_PAGE.length; + outputPacketSize += oggIdHeaderPageSize + OGG_DEFAULT_COMMENT_HEADER_PAGE.length; + } + // Resample the little endian input and update the output buffers. ByteBuffer buffer = replaceOutputBuffer(outputPacketSize); - // Capture Pattern for Page [OggS] - buffer.put((byte) 'O'); - buffer.put((byte) 'g'); - buffer.put((byte) 'g'); - buffer.put((byte) 'S'); - - // StreamStructure Version - buffer.put((byte) 0); - - // header_type_flag - buffer.put((byte) 0x00); + // If first audio sample in stream then insert Ogg ID Header and Comment Header Pages + if (pageSequenceNumber == FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER) { + if (providedOggIdHeaderPayloadBytes != null) { + writeOggIdHeaderPage(buffer, /* idHeaderPayloadBytes= */ providedOggIdHeaderPayloadBytes); + } else { + // Write default Ogg ID Header Payload + buffer.put(OGG_DEFAULT_ID_HEADER_PAGE); + } + buffer.put(OGG_DEFAULT_COMMENT_HEADER_PAGE); + } // granule_position int numSamples = OpusUtil.parsePacketAudioSampleCount(inputBuffer); granulePosition += numSamples; - buffer.putLong(granulePosition); - // bitstream_serial_number - buffer.putInt(0); - - // page_sequence_number - buffer.putInt(pageSequenceNumber); - pageSequenceNumber++; - - // CRC_checksum - buffer.putInt(0); - - // number_page_segments - buffer.put((byte) numSegments); + writeOggPacketHeader( + buffer, granulePosition, pageSequenceNumber, numSegments, /* isIdHeaderPacket= */ false); // Segment_table int bytesLeft = inputBufferSize; @@ -131,6 +166,7 @@ public final class OggOpusAudioPacketizer { } } + // Write Opus audio data for (int i = position; i < limit; i++) { buffer.put(inputBuffer.get(i)); } @@ -138,16 +174,103 @@ public final class OggOpusAudioPacketizer { inputBuffer.position(inputBuffer.limit()); buffer.flip(); + int checksum; + if (pageSequenceNumber == FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER) { + checksum = + Util.crc32( + buffer.array(), + /* start= */ buffer.arrayOffset() + + oggIdHeaderPageSize + + OGG_DEFAULT_COMMENT_HEADER_PAGE.length, + /* end= */ buffer.limit() - buffer.position(), + /* initialValue= */ 0); + buffer.putInt( + oggIdHeaderPageSize + OGG_DEFAULT_COMMENT_HEADER_PAGE.length + CHECKSUM_INDEX, checksum); + } else { + checksum = + Util.crc32( + buffer.array(), + /* start= */ buffer.arrayOffset(), + /* end= */ buffer.limit() - buffer.position(), + /* initialValue= */ 0); + buffer.putInt(CHECKSUM_INDEX, checksum); + } + + // Increase pageSequenceNumber for next packet + pageSequenceNumber++; + + return buffer; + } + + /** + * Write Ogg ID Header Page packet to {@link ByteBuffer}. + * + * @param buffer to write into. + * @param idHeaderPayloadBytes containing the Ogg ID Header Page payload. + */ + private void writeOggIdHeaderPage(ByteBuffer buffer, byte[] idHeaderPayloadBytes) { + // TODO(b/290195621): Use starting position to calculate correct 'pre-skip' value + writeOggPacketHeader( + buffer, + /* granulePosition= */ 0, + /* pageSequenceNumber= */ 0, + /* numberPageSegments= */ 1, + /* isIdHeaderPacket= */ true); + buffer.put(UnsignedBytes.checkedCast(idHeaderPayloadBytes.length)); + buffer.put(idHeaderPayloadBytes); int checksum = Util.crc32( buffer.array(), - buffer.arrayOffset(), - buffer.limit() - buffer.position(), + /* start= */ buffer.arrayOffset(), + /* end= */ OGG_PACKET_HEADER_LENGTH + idHeaderPayloadBytes.length, /* initialValue= */ 0); - buffer.putInt(22, checksum); - buffer.position(0); + buffer.putInt(/* index= */ CHECKSUM_INDEX, checksum); + buffer.position(OGG_PACKET_HEADER_LENGTH + idHeaderPayloadBytes.length); + } - return buffer; + /** + * Write header for an Ogg Page Packet to {@link ByteBuffer}. + * + * @param byteBuffer to write unto. + * @param granulePosition is the number of audio samples in the stream up to and including this + * packet. + * @param pageSequenceNumber of the page this header is for. + * @param numberPageSegments the data of this Ogg page will span. + * @param isIdHeaderPacket where if this header is start of the bitstream. + */ + private void writeOggPacketHeader( + ByteBuffer byteBuffer, + long granulePosition, + int pageSequenceNumber, + int numberPageSegments, + boolean isIdHeaderPacket) { + // Capture Pattern for Ogg Page [OggS] + byteBuffer.put((byte) 'O'); + byteBuffer.put((byte) 'g'); + byteBuffer.put((byte) 'g'); + byteBuffer.put((byte) 'S'); + + // StreamStructure Version + byteBuffer.put((byte) 0); + + // Header-type + byteBuffer.put(isIdHeaderPacket ? (byte) 0x02 : (byte) 0x00); + + // Granule_position + byteBuffer.putLong(granulePosition); + + // bitstream_serial_number + byteBuffer.putInt(SERIAL_NUMBER); + + // Page_sequence_number + byteBuffer.putInt(pageSequenceNumber); + + // CRC_checksum + // Will be overwritten with calculated checksum after rest of page is written to buffer. + byteBuffer.putInt(0); + + // Number_page_segments + byteBuffer.put(UnsignedBytes.checkedCast(numberPageSegments)); } /** diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java index 89d695d4a1..e192905605 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java @@ -720,6 +720,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { bypassBatchBuffer.clear(); bypassSampleBuffer.clear(); bypassSampleBufferPending = false; + oggOpusAudioPacketizer.reset(); } else { flushOrReinitializeCodec(); } @@ -2349,7 +2350,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (inputFormat != null && inputFormat.sampleMimeType != null && inputFormat.sampleMimeType.equals(MimeTypes.AUDIO_OPUS)) { - oggOpusAudioPacketizer.packetize(bypassSampleBuffer); + oggOpusAudioPacketizer.packetize(bypassSampleBuffer, inputFormat.initializationData); } if (!bypassBatchBuffer.append(bypassSampleBuffer)) { bypassSampleBufferPending = true; diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/OpusUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/OpusUtil.java index f0e8203325..ca34bb5b80 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/OpusUtil.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/OpusUtil.java @@ -76,15 +76,45 @@ public class OpusUtil { */ public static int parseOggPacketAudioSampleCount(ByteBuffer buffer) { // RFC 3433 section 6 - The Ogg page format. - int numPageSegments = buffer.get(/* index= */ 26); - int indexFirstOpusPacket = 27 + numPageSegments; // Skip Ogg header and segment table. + int preAudioPacketByteCount = parseOggPacketForPreAudioSampleByteCount(buffer); + int numPageSegments = buffer.get(/* index= */ 26 + preAudioPacketByteCount); + // Skip Ogg header + segment table. + int indexFirstOpusPacket = 27 + numPageSegments + preAudioPacketByteCount; long packetDurationUs = getPacketDurationUs( buffer.get(indexFirstOpusPacket), - buffer.limit() > 1 ? buffer.get(indexFirstOpusPacket + 1) : 0); + buffer.limit() - indexFirstOpusPacket > 1 ? buffer.get(indexFirstOpusPacket + 1) : 0); return (int) (packetDurationUs * SAMPLE_RATE / C.MICROS_PER_SECOND); } + /** + * Calculate the offset from the start of the buffer to audio sample Ogg packets. + * + * @param buffer containing the Ogg Encapsulated Opus audio bitstream. + * @return the offset before the Ogg packet containing audio samples. + */ + public static int parseOggPacketForPreAudioSampleByteCount(ByteBuffer buffer) { + // Parse Ogg Packet Type from Header at index 5 + if ((buffer.get(/* index= */ 5) & 0x02) == 0) { + // Ogg Page packet header type is not beginning of logical stream. Must be an Audio page. + return 0; + } + // ID Header Page size is Ogg packet header size + sum(lacing values: 1..number_page_segments). + int idHeaderPageSize = 28; + int idHeaderPageNumOfSegments = buffer.get(/* index= */ 26); + for (int i = 0; i < idHeaderPageNumOfSegments; i++) { + idHeaderPageSize += buffer.get(/* index= */ 27 + i); + } + // Comment Header Page size is Ogg packet header size + sum(lacing values: + // 1..number_page_segments). + int commentHeaderPageSize = 28; + int commentHeaderPageSizeNumOfSegments = buffer.get(/* index= */ idHeaderPageSize + 26); + for (int i = 0; i < commentHeaderPageSizeNumOfSegments; i++) { + commentHeaderPageSize += buffer.get(/* index= */ idHeaderPageSize + 27 + i); + } + return idHeaderPageSize + commentHeaderPageSize; + } + /** * Returns the number of audio samples in the given audio packet. * diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/OpusUtilTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/OpusUtilTest.java index 48c74327c2..7d4e36be2e 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/OpusUtilTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/OpusUtilTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import androidx.media3.common.C; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.primitives.Bytes; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.List; @@ -30,34 +31,140 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class OpusUtilTest { - private static final byte[] HEADER = - new byte[] {79, 112, 117, 115, 72, 101, 97, 100, 0, 2, 1, 56, 0, 0, -69, -128, 0, 0, 0}; + /** Ogg Packet Header in accordance with RFC 3533 for an Ogg ID Header Page. */ + private static final byte[] OGG_ID_HEADER_PACKET_HEADER = + new byte[] { + 79, 103, 103, 83, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, -43, -59, -9, 1, + 19 + }; - private static final int HEADER_PRE_SKIP_SAMPLES = 14337; - private static final byte[] HEADER_PRE_SKIP_BYTES = - buildNativeOrderByteArray(sampleCountToNanoseconds(HEADER_PRE_SKIP_SAMPLES)); + /** Payload for Ogg ID Header Page in accordance with RFC 7845. */ + private static final byte[] OGG_ID_HEADER_PAYLOAD = + new byte[] {79, 112, 117, 115, 72, 101, 97, 100, 1, 2, 56, 1, -128, -69, 0, 0, 0, 0, 0}; + private static final int OGG_ID_HEADER_PRE_SKIP_SAMPLES = 312; + private static final byte[] OGG_ID_HEADER_PRE_SKIP_BYTES = + buildNativeOrderByteArray(sampleCountToNanoseconds(OGG_ID_HEADER_PRE_SKIP_SAMPLES)); private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840; private static final byte[] DEFAULT_SEEK_PRE_ROLL_BYTES = buildNativeOrderByteArray(sampleCountToNanoseconds(DEFAULT_SEEK_PRE_ROLL_SAMPLES)); + /** Ogg Packet Header in accordance with RFC 3533 for an Ogg Comment Header Page. */ + private static final byte[] OGG_COMMENT_HEADER_PACKET_HEADER = + new byte[] { + 79, 103, 103, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 11, -103, 87, 83, 1, + 16 + }; + + /** Payload for Ogg Comment Header Page with empty vendor and comment sections. */ + private static final byte[] OGG_COMMENT_HEADER_PACKET_PAYLOAD = + new byte[] {79, 112, 117, 115, 84, 97, 103, 115, 0, 0, 0, 0, 0, 0, 0, 0}; + + /** Ogg Packet Header for an page of an Opus audio sample contained in a single segment. */ + private static final byte[] OGG_OPUS_PACKET_HEADER_SINGLE_SEGMENT = + new byte[] { + 79, 103, 103, 83, 0, 0, -32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 54, -31, -124, 22, + 1, -22 + }; + + /** Ogg Packet Header for an page of an Opus audio sample that takes up multiple segments. */ + private static final byte[] OGG_OPUS_PACKET_HEADER_MULTIPLE_SEGMENTS = + new byte[] { + 79, 103, 103, 83, 0, 0, -32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 54, -31, -124, 22, + 2, -1, -22 + }; + + private static final byte[] OGG_OPUS_PACKET_PAYLOAD_CODE_ZERO_TOC = getBytesFromHexString("04"); + private static final byte[] OGG_OPUS_PACKET_PAYLOAD_CODE_THREE_TOC = + getBytesFromHexString("078C"); + @Test public void buildInitializationData_returnsExpectedHeaderWithPreSkipAndPreRoll() { - List initializationData = OpusUtil.buildInitializationData(HEADER); + List initializationData = OpusUtil.buildInitializationData(OGG_ID_HEADER_PAYLOAD); assertThat(initializationData).hasSize(3); - assertThat(initializationData.get(0)).isEqualTo(HEADER); - assertThat(initializationData.get(1)).isEqualTo(HEADER_PRE_SKIP_BYTES); + assertThat(initializationData.get(0)).isEqualTo(OGG_ID_HEADER_PAYLOAD); + assertThat(initializationData.get(1)).isEqualTo(OGG_ID_HEADER_PRE_SKIP_BYTES); assertThat(initializationData.get(2)).isEqualTo(DEFAULT_SEEK_PRE_ROLL_BYTES); } @Test public void getChannelCount_returnsChannelCount() { - int channelCount = OpusUtil.getChannelCount(HEADER); + int channelCount = OpusUtil.getChannelCount(OGG_ID_HEADER_PAYLOAD); assertThat(channelCount).isEqualTo(2); } + @Test + public void parseOggPacketForPreAudioSampleByteCount_returnsExpectedByteCount() { + byte[] packetData = + Bytes.concat( + OGG_ID_HEADER_PACKET_HEADER, + OGG_ID_HEADER_PAYLOAD, + OGG_COMMENT_HEADER_PACKET_HEADER, + OGG_COMMENT_HEADER_PACKET_PAYLOAD, + OGG_OPUS_PACKET_HEADER_SINGLE_SEGMENT, + OGG_OPUS_PACKET_PAYLOAD_CODE_ZERO_TOC); + ByteBuffer preAudioOggPacketsByteBuffer = ByteBuffer.wrap(packetData); + + int preAudioSampleByteCount = + OpusUtil.parseOggPacketForPreAudioSampleByteCount(preAudioOggPacketsByteBuffer); + + assertThat(preAudioSampleByteCount).isEqualTo(91); + } + + @Test + public void parseOggPacketAudioSampleCount_withCodeZeroToc_returnsExpectedAudioSampleCount() { + byte[] packetData = + Bytes.concat( + OGG_ID_HEADER_PACKET_HEADER, + OGG_ID_HEADER_PAYLOAD, + OGG_COMMENT_HEADER_PACKET_HEADER, + OGG_COMMENT_HEADER_PACKET_PAYLOAD, + OGG_OPUS_PACKET_HEADER_SINGLE_SEGMENT, + OGG_OPUS_PACKET_PAYLOAD_CODE_ZERO_TOC); + ByteBuffer oggPacketsByteBuffer = ByteBuffer.wrap(packetData); + + int audioSampleCount = OpusUtil.parseOggPacketAudioSampleCount(oggPacketsByteBuffer); + + assertThat(audioSampleCount).isEqualTo(480); + } + + @Test + public void + parseOggPacketAudioSampleCount_withMultipleOggPageSegments_returnsExpectedAudioSampleCount() { + byte[] packetData = + Bytes.concat( + OGG_ID_HEADER_PACKET_HEADER, + OGG_ID_HEADER_PAYLOAD, + OGG_COMMENT_HEADER_PACKET_HEADER, + OGG_COMMENT_HEADER_PACKET_PAYLOAD, + OGG_OPUS_PACKET_HEADER_MULTIPLE_SEGMENTS, + OGG_OPUS_PACKET_PAYLOAD_CODE_ZERO_TOC); + ByteBuffer oggPacketsByteBuffer = ByteBuffer.wrap(packetData); + + int audioSampleCount = OpusUtil.parseOggPacketAudioSampleCount(oggPacketsByteBuffer); + + assertThat(audioSampleCount).isEqualTo(480); + } + + @Test + public void parseOggPacketAudioSampleCount_withCodeThreeToc_returnsExpectedAudioSampleCount() { + byte[] packetData = + Bytes.concat( + OGG_ID_HEADER_PACKET_HEADER, + OGG_ID_HEADER_PAYLOAD, + OGG_COMMENT_HEADER_PACKET_HEADER, + OGG_COMMENT_HEADER_PACKET_PAYLOAD, + OGG_OPUS_PACKET_HEADER_SINGLE_SEGMENT, + OGG_OPUS_PACKET_PAYLOAD_CODE_THREE_TOC); + ByteBuffer oggPacketsByteBuffer = ByteBuffer.wrap(packetData); + + int audioSampleCount = OpusUtil.parseOggPacketAudioSampleCount(oggPacketsByteBuffer); + + assertThat(audioSampleCount).isEqualTo(5760); + } + @Test public void getPacketDurationUs_code0_returnsExpectedDuration() { long config0DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("04")); diff --git a/libraries/test_data/src/test/assets/playbackdumps/ogg/bear.opus.oggOpus.dump b/libraries/test_data/src/test/assets/playbackdumps/ogg/bear.opus.oggOpus.dump index 4af139df56..cdaff10363 100644 --- a/libraries/test_data/src/test/assets/playbackdumps/ogg/bear.opus.oggOpus.dump +++ b/libraries/test_data/src/test/assets/playbackdumps/ogg/bear.opus.oggOpus.dump @@ -1,6 +1,6 @@ SinkDump (OggOpus): buffers.length = 9 - buffers[0] = length 4046, hash 68FA8318 + buffers[0] = length 4137, hash 9776A1C3 buffers[1] = length 3848, hash B3105060 buffers[2] = length 3747, hash 63B6648B buffers[3] = length 3752, hash B5C28B9D