From 61b7dfd7ba2eacc8197350e85d387a746eabf356 Mon Sep 17 00:00:00 2001 From: dancho Date: Thu, 23 Jan 2025 04:44:00 -0800 Subject: [PATCH] Add ObuParser to split a ByteBuffer into AV1 OBUs PiperOrigin-RevId: 718813571 --- .../androidx/media3/container/ObuParser.java | 117 ++++++++++++++++++ .../media3/container/ObuParserTest.java | 71 +++++++++++ 2 files changed, 188 insertions(+) create mode 100644 libraries/container/src/main/java/androidx/media3/container/ObuParser.java create mode 100644 libraries/container/src/test/java/androidx/media3/container/ObuParserTest.java diff --git a/libraries/container/src/main/java/androidx/media3/container/ObuParser.java b/libraries/container/src/main/java/androidx/media3/container/ObuParser.java new file mode 100644 index 0000000000..d985acdafa --- /dev/null +++ b/libraries/container/src/main/java/androidx/media3/container/ObuParser.java @@ -0,0 +1,117 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.container; + +import androidx.media3.common.util.UnstableApi; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Utility methods for parsing AV1 Open Bitstream Units or OBUs. + * + *

AV1 Bitstream and Decoding Process + * Specification + */ +@UnstableApi +public final class ObuParser { + + /** OBU type sequence header. */ + public static final int OBU_SEQUENCE_HEADER = 1; + + /** OBU type temporal delimiter. */ + public static final int OBU_TEMPORAL_DELIMITER = 2; + + /** OBU type frame header. */ + public static final int OBU_FRAME_HEADER = 3; + + /** OBU type frame. */ + public static final int OBU_FRAME = 6; + + /** OBU type padding. */ + public static final int OBU_PADDING = 15; + + /** Open bitstream unit. */ + public static final class Obu { + + /** The OBU type. See {@code obu_type} in the AV1 spec. */ + public final int type; + + /** The OBU data, excluding the header. */ + public final ByteBuffer payload; + + private Obu(int type, ByteBuffer payload) { + this.type = type; + this.payload = payload; + } + } + + /** + * Splits the input sample into a list of OBUs. + * + *

Expects the AV1 sample format specified by the AV1 Codec ISO Media File Format + * Binding. That is, each OBU has the {@code obu_has_size_field} set to 1 except for the last + * OBU in the sample, for which {@code obu_has_size_field} may be set to 0. + * + * @param sample The sample data. + * @return The list of OBUs contained within the sample data. + */ + public static List split(ByteBuffer sample) { + // Create a read-only buffer that shares content with the sample to avoid modifying the + // input buffer's position, mark or limit. + ByteBuffer readOnlySample = sample.asReadOnlyBuffer(); + List obuList = new ArrayList<>(); + while (readOnlySample.hasRemaining()) { + int headerByte = readOnlySample.get(); + int obuType = (headerByte >> 3) & 0xF; + int extensionFlag = (headerByte >> 2) & 0x1; + if (extensionFlag != 0) { + readOnlySample.get(); // skip obu_extension_header() + } + int obuHasSizeField = (headerByte >> 1) & 0x1; + int obuSize; + if (obuHasSizeField != 0) { + obuSize = leb128(readOnlySample); + } else { + // Only the last sample is allowed to have obu_has_size_field == 0, and the size is assumed + // to fill the remainder of the sample. + obuSize = readOnlySample.remaining(); + } + ByteBuffer payload = readOnlySample.duplicate(); + payload.limit(readOnlySample.position() + obuSize); + obuList.add(new Obu(obuType, payload)); + readOnlySample.position(readOnlySample.position() + obuSize); + } + return obuList; + } + + private static int leb128(ByteBuffer data) { + int value = 0; + for (int i = 0; i < 8; i++) { + int leb128Byte = data.get(); + value |= ((leb128Byte & 0x7F) << (i * 7)); + if ((leb128Byte & 0x80) == 0) { + break; + } + } + return value; + } + + private ObuParser() { + // Prevent instantiation. + } +} diff --git a/libraries/container/src/test/java/androidx/media3/container/ObuParserTest.java b/libraries/container/src/test/java/androidx/media3/container/ObuParserTest.java new file mode 100644 index 0000000000..3e7d0cb4e1 --- /dev/null +++ b/libraries/container/src/test/java/androidx/media3/container/ObuParserTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.container; + +import static androidx.media3.container.ObuParser.OBU_FRAME; +import static androidx.media3.container.ObuParser.OBU_FRAME_HEADER; +import static androidx.media3.container.ObuParser.OBU_PADDING; +import static androidx.media3.container.ObuParser.OBU_SEQUENCE_HEADER; +import static androidx.media3.container.ObuParser.OBU_TEMPORAL_DELIMITER; +import static androidx.media3.test.utils.TestUtil.createByteArray; +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.nio.ByteBuffer; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link ObuParser} */ +@RunWith(AndroidJUnit4.class) +public class ObuParserTest { + private static final ByteBuffer SEQUENCE_HEADER_AND_FRAME = + ByteBuffer.wrap( + createByteArray( + 0x0A, 0x0E, 0x00, 0x00, 0x00, 0x24, 0xC6, 0xAB, 0xDF, 0x3E, 0xFE, 0x24, 0x04, 0x04, + 0x04, 0x10, 0x32, 0x32, 0x10, 0x00, 0xC8, 0xC6, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, + 0x12, 0x03, 0xCE, 0x0A, 0x5C, 0x9B, 0xB6, 0x7C, 0x34, 0x88, 0x82, 0x3E, 0x0D, 0x3E, + 0xC2, 0x98, 0x91, 0x6A, 0x5C, 0x80, 0x03, 0xCE, 0x0A, 0x5C, 0x9B, 0xB6, 0x7C, 0x48, + 0x35, 0x54, 0xD8, 0x9D, 0x6C, 0x37, 0xD3, 0x4C, 0x4E, 0xD4, 0x6F, 0xF4)); + + private static final ByteBuffer DELIMITER_AND_HEADER_AND_PADDING_WITH_EXTENSION_AND_MISSING_SIZE = + ByteBuffer.wrap(createByteArray(0x16, 0x00, 0x00, 0x1A, 0x01, 0xC8, 0x78, 0xFF, 0xFF, 0xFF)); + + @Test + public void split_sequenceHeaderAndFrame_parsesCorrectTypesAndSizes() { + List obuList = ObuParser.split(SEQUENCE_HEADER_AND_FRAME); + + assertThat(obuList).hasSize(2); + assertThat(obuList.get(0).type).isEqualTo(OBU_SEQUENCE_HEADER); + assertThat(obuList.get(0).payload.remaining()).isEqualTo(14); + assertThat(obuList.get(1).type).isEqualTo(OBU_FRAME); + assertThat(obuList.get(1).payload.remaining()).isEqualTo(50); + } + + @Test + public void split_delimiterAndHeaderAndPadding_parsesCorrectTypesAndSizes() { + List obuList = + ObuParser.split(DELIMITER_AND_HEADER_AND_PADDING_WITH_EXTENSION_AND_MISSING_SIZE); + + assertThat(obuList).hasSize(3); + assertThat(obuList.get(0).type).isEqualTo(OBU_TEMPORAL_DELIMITER); + assertThat(obuList.get(0).payload.remaining()).isEqualTo(0); + assertThat(obuList.get(1).type).isEqualTo(OBU_FRAME_HEADER); + assertThat(obuList.get(1).payload.remaining()).isEqualTo(1); + assertThat(obuList.get(2).type).isEqualTo(OBU_PADDING); + assertThat(obuList.get(2).payload.remaining()).isEqualTo(3); + } +}