From 80a734f4f1217ed7d339c03129a9b4e8a7dc07f2 Mon Sep 17 00:00:00 2001 From: dancho Date: Mon, 27 Jan 2025 03:16:41 -0800 Subject: [PATCH] AV1 bitstream parser that identifies frames with no dependencies Usage: * call queueInputBuffer() with initialization data or sample data in decode order - updates the parser state * call sampleLimitAfterSkippingNonReferenceFrame to identify if the sample contains a frame without dependencies PiperOrigin-RevId: 720098835 --- .../video/Av1SampleDependencyParser.java | 94 ++++++++++ .../video/Av1SampleDependencyParserTest.java | 160 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/Av1SampleDependencyParser.java create mode 100644 libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/Av1SampleDependencyParserTest.java 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); + } +}