diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/Av1SampleDependencyParser.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/Av1SampleDependencyParser.java
new file mode 100644
index 0000000000..4f0df3f4de
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/Av1SampleDependencyParser.java
@@ -0,0 +1,94 @@
+/*
+ * 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.exoplayer.video;
+
+import static androidx.media3.container.ObuParser.OBU_FRAME;
+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.container.ObuParser.split;
+
+import androidx.annotation.Nullable;
+import androidx.media3.container.ObuParser;
+import androidx.media3.container.ObuParser.FrameHeader;
+import androidx.media3.container.ObuParser.SequenceHeader;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+/** An AV1 bitstream parser that identifies frames that are not depended on. */
+/* package */ final class Av1SampleDependencyParser {
+ @Nullable private SequenceHeader sequenceHeader;
+
+ /**
+ * Returns the new sample {@linkplain ByteBuffer#limit() limit} after deleting any frames that are
+ * not used as reference.
+ *
+ *
Each AV1 temporal unit must have exactly one shown frame. Other frames in the temporal unit
+ * that aren't shown are used as reference, but the shown frame may not be used as reference.
+ * Frequently, the shown frame is the last frame in the temporal unit.
+ *
+ *
If the last frame in the temporal unit is a non-reference {@link ObuParser#OBU_FRAME}, this
+ * method returns a new {@link ByteBuffer#limit()} value that would leave only the frames used as
+ * reference in the input {@code sample}.
+ *
+ *
See Ordering of OBUs.
+ *
+ * @param sample The sample data for one AV1 temporal unit.
+ */
+ public int sampleLimitAfterSkippingNonReferenceFrame(ByteBuffer sample) {
+ List obuList = split(sample);
+ updateSequenceHeaders(obuList);
+ int skippedFramesCount = 0;
+ int last = obuList.size() - 1;
+ while (last >= 0 && canSkipObu(obuList.get(last))) {
+ if (obuList.get(last).type == OBU_FRAME) {
+ skippedFramesCount++;
+ }
+ last--;
+ }
+ if (skippedFramesCount > 1) {
+ return sample.limit();
+ }
+ if (last >= 0) {
+ return obuList.get(last).payload.limit();
+ }
+ return sample.position();
+ }
+
+ /** Updates the parser state with the next sample data. */
+ public void queueInputBuffer(ByteBuffer sample) {
+ updateSequenceHeaders(split(sample));
+ }
+
+ private boolean canSkipObu(ObuParser.Obu obu) {
+ if (obu.type == OBU_TEMPORAL_DELIMITER || obu.type == OBU_PADDING) {
+ return true;
+ }
+ if (obu.type == OBU_FRAME && sequenceHeader != null) {
+ FrameHeader frameHeader = FrameHeader.parse(sequenceHeader, obu);
+ return frameHeader != null && !frameHeader.isDependedOn();
+ }
+ return false;
+ }
+
+ private void updateSequenceHeaders(List obuList) {
+ for (int i = 0; i < obuList.size(); ++i) {
+ if (obuList.get(i).type == OBU_SEQUENCE_HEADER) {
+ sequenceHeader = SequenceHeader.parse(obuList.get(i));
+ }
+ }
+ }
+}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/Av1SampleDependencyParserTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/Av1SampleDependencyParserTest.java
new file mode 100644
index 0000000000..825b7dbd49
--- /dev/null
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/Av1SampleDependencyParserTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.exoplayer.video;
+
+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 org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Av1SampleDependencyParser} */
+@RunWith(AndroidJUnit4.class)
+public class Av1SampleDependencyParserTest {
+
+ private static final byte[] sequenceHeader =
+ createByteArray(
+ 0x0A, 0x0E, 0x00, 0x00, 0x00, 0x24, 0xC6, 0xAB, 0xDF, 0x3E, 0xFE, 0x24, 0x04, 0x04, 0x04,
+ 0x10);
+ private static final byte[] dependedOnFrame =
+ createByteArray(
+ 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 byte[] notDependedOnFrame =
+ createByteArray(
+ 0x32, 0x1A, 0x30, 0xC0, 0x00, 0x1D, 0x66, 0x68, 0x46, 0xC9, 0x38, 0x00, 0x60, 0x10, 0x20,
+ 0x80, 0x20, 0x00, 0x00, 0x01, 0x8B, 0x7A, 0x87, 0xF9, 0xAA, 0x2D, 0x0F, 0x2C);
+
+ private static final byte[] temporalDelimiter = createByteArray(0x12, 0x00);
+
+ private static final byte[] padding = createByteArray(0x7a, 0x02, 0xFF, 0xFF);
+
+ @Test
+ public void sampleLimitAfterSkippingNonReferenceFrame_sampleIsDependedOn_returnsFullSample() {
+ ByteBuffer sample = ByteBuffer.allocate(128);
+ sample.put(sequenceHeader);
+ sample.put(dependedOnFrame);
+ sample.flip();
+ Av1SampleDependencyParser av1SampleDependencyParser = new Av1SampleDependencyParser();
+
+ int sampleLimitAfterSkippingNonReferenceFrames =
+ av1SampleDependencyParser.sampleLimitAfterSkippingNonReferenceFrame(sample);
+
+ assertThat(sampleLimitAfterSkippingNonReferenceFrames)
+ .isEqualTo(sequenceHeader.length + dependedOnFrame.length);
+ }
+
+ @Test
+ public void
+ sampleLimitAfterSkippingNonReferenceFrame_sampleIsNotDependedOn_returnsClippedSample() {
+ ByteBuffer sample = ByteBuffer.allocate(128);
+ sample.put(sequenceHeader);
+ sample.put(notDependedOnFrame);
+ sample.flip();
+ Av1SampleDependencyParser av1SampleDependencyParser = new Av1SampleDependencyParser();
+
+ int sampleLimitAfterSkippingNonReferenceFrames =
+ av1SampleDependencyParser.sampleLimitAfterSkippingNonReferenceFrame(sample);
+
+ assertThat(sampleLimitAfterSkippingNonReferenceFrames).isEqualTo(sequenceHeader.length);
+ }
+
+ @Test
+ public void
+ sampleLimitAfterSkippingNonReferenceFrame_queueSequenceHeaderSeparately_returnsEmptySample() {
+ ByteBuffer header = ByteBuffer.wrap(sequenceHeader);
+ ByteBuffer frame = ByteBuffer.wrap(notDependedOnFrame);
+ Av1SampleDependencyParser av1SampleDependencyParser = new Av1SampleDependencyParser();
+
+ av1SampleDependencyParser.queueInputBuffer(header);
+ int sampleLimitAfterSkippingNonReferenceFrames =
+ av1SampleDependencyParser.sampleLimitAfterSkippingNonReferenceFrame(frame);
+
+ assertThat(sampleLimitAfterSkippingNonReferenceFrames).isEqualTo(0);
+ }
+
+ @Test
+ public void
+ sampleLimitAfterSkippingNonReferenceFrame_withTemporalDelimiterAndPadding_returnsEmptySample() {
+ ByteBuffer header = ByteBuffer.wrap(sequenceHeader);
+ ByteBuffer sample = ByteBuffer.allocate(128);
+ sample.put(temporalDelimiter);
+ sample.put(notDependedOnFrame);
+ sample.put(padding);
+ sample.flip();
+ Av1SampleDependencyParser av1SampleDependencyParser = new Av1SampleDependencyParser();
+
+ av1SampleDependencyParser.queueInputBuffer(header);
+ int sampleLimitAfterSkippingNonReferenceFrames =
+ av1SampleDependencyParser.sampleLimitAfterSkippingNonReferenceFrame(sample);
+
+ assertThat(sampleLimitAfterSkippingNonReferenceFrames).isEqualTo(0);
+ }
+
+ @Test
+ public void sampleLimitAfterSkippingNonReferenceFrame_withMultipleFrames_returnsClippedSample() {
+ ByteBuffer header = ByteBuffer.wrap(sequenceHeader);
+ ByteBuffer sample = ByteBuffer.allocate(128);
+ sample.put(temporalDelimiter);
+ sample.put(padding);
+ sample.put(dependedOnFrame);
+ sample.put(notDependedOnFrame);
+ sample.put(padding);
+ sample.flip();
+ Av1SampleDependencyParser av1SampleDependencyParser = new Av1SampleDependencyParser();
+
+ av1SampleDependencyParser.queueInputBuffer(header);
+ int sampleLimitAfterSkippingNonReferenceFrames =
+ av1SampleDependencyParser.sampleLimitAfterSkippingNonReferenceFrame(sample);
+
+ assertThat(sampleLimitAfterSkippingNonReferenceFrames)
+ .isEqualTo(temporalDelimiter.length + padding.length + dependedOnFrame.length);
+ }
+
+ @Test
+ public void sampleLimitAfterSkippingNonReferenceFrame_withMissingHeader_returnsFullSample() {
+ ByteBuffer frame = ByteBuffer.wrap(notDependedOnFrame);
+ Av1SampleDependencyParser av1SampleDependencyParser = new Av1SampleDependencyParser();
+
+ int sampleLimitAfterSkippingNonReferenceFrames =
+ av1SampleDependencyParser.sampleLimitAfterSkippingNonReferenceFrame(frame);
+
+ assertThat(sampleLimitAfterSkippingNonReferenceFrames).isEqualTo(notDependedOnFrame.length);
+ }
+
+ @Test
+ public void
+ sampleLimitAfterSkippingNonReferenceFrame_withTwoNonDependedOnFrames_returnsFullSample() {
+ ByteBuffer header = ByteBuffer.wrap(sequenceHeader);
+ ByteBuffer sample = ByteBuffer.allocate(128);
+ sample.put(notDependedOnFrame);
+ sample.put(notDependedOnFrame);
+ sample.flip();
+ Av1SampleDependencyParser av1SampleDependencyParser = new Av1SampleDependencyParser();
+
+ av1SampleDependencyParser.queueInputBuffer(header);
+ int sampleLimitAfterSkippingNonReferenceFrames =
+ av1SampleDependencyParser.sampleLimitAfterSkippingNonReferenceFrame(sample);
+
+ assertThat(sampleLimitAfterSkippingNonReferenceFrames)
+ .isEqualTo(notDependedOnFrame.length + notDependedOnFrame.length);
+ }
+}