Commit 727aded3 by michaelkatz Committed by Tianyi Feng

Prepend Ogg ID and Comment Header Pages to offloaded Opus stream

Add Ogg ID Header and Comment Header Pages to the Ogg encapsulated Opus for offload playback. This further matches the RFC 7845 spec and provides initialization data to decoders.

PiperOrigin-RevId: 548080222
(cherry picked from commit 4c894aa6a961fb53e92bf01361f8459d65055af3)
parent 1e93c828
......@@ -709,6 +709,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
bypassBatchBuffer.clear();
bypassSampleBuffer.clear();
bypassSampleBufferPending = false;
oggOpusAudioPacketizer.reset();
} else {
flushOrReinitializeCodec();
}
......@@ -2338,7 +2339,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)) {
......
......@@ -82,16 +82,46 @@ 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.
*
* <p>The buffer's position is not modified.
......
......@@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.common.primitives.Bytes;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
......@@ -30,35 +31,141 @@ 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<byte[]> initializationData = OpusUtil.buildInitializationData(HEADER);
List<byte[]> 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"));
long config1DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("0C"));
......
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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment