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);
+ }
+}