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
This commit is contained in:
dancho 2025-01-27 03:16:41 -08:00 committed by Copybara-Service
parent c3962d2fe6
commit 80a734f4f1
2 changed files with 254 additions and 0 deletions

View File

@ -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.
*
* <p>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.
*
* <p>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}.
*
* <p>See <a href=https://aomediacodec.github.io/av1-spec/#ordering-of-obus>Ordering of OBUs</a>.
*
* @param sample The sample data for one AV1 temporal unit.
*/
public int sampleLimitAfterSkippingNonReferenceFrame(ByteBuffer sample) {
List<ObuParser.Obu> 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<ObuParser.Obu> obuList) {
for (int i = 0; i < obuList.size(); ++i) {
if (obuList.get(i).type == OBU_SEQUENCE_HEADER) {
sequenceHeader = SequenceHeader.parse(obuList.get(i));
}
}
}
}

View File

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