Synthesize CSD data from AV1 sample in Muxer.

Synthesize CSD data from the key frame bit stream if CSD is not available for AV1 in Muxer.

PiperOrigin-RevId: 736815774
This commit is contained in:
Googler 2025-03-14 04:14:27 -07:00 committed by Copybara-Service
parent fe8163838e
commit 641434ff31
11 changed files with 548 additions and 60 deletions

View File

@ -43,6 +43,9 @@ public final class ObuParser {
/** OBU type frame header. */
public static final int OBU_FRAME_HEADER = 3;
/** OBU type metadata. */
public static final int OBU_METADATA = 5;
/** OBU type frame. */
public static final int OBU_FRAME = 6;
@ -136,6 +139,48 @@ public final class ObuParser {
/** See {@code OrderHintBits}. */
public final int orderHintBits;
/** See {@code seq_profile}. */
public final int seqProfile;
/** See {@code seq_level_idx}. */
public final int seqLevelIdx0;
/** See {@code seq_tier}. */
public final int seqTier0;
/** See {@code initial_display_delay_present}. */
public final boolean initialDisplayDelayPresentFlag;
/** See {@code initial_display_delay_minus_one}. */
public final int initialDisplayDelayMinus1;
/** See {@code high_bitdepth}. */
public final boolean highBitdepth;
/** See {@code twelve_bit}. */
public final boolean twelveBit;
/** See {@code mono_chrome}. */
public final boolean monochrome;
/** See {@code subsampling_x}. */
public final boolean subsamplingX;
/** See {@code subsampling_Y}. */
public final boolean subsamplingY;
/** See {@code chroma_sample_position}. */
public final int chromaSamplePosition;
/** See {@code color_primaries}. */
public final byte colorPrimaries;
/** See {@code transfer_characteristics}. */
public final byte transferCharacteristics;
/** See {@code matrix_coefficients}. */
public final byte matrixCoefficients;
/**
* Returns a {@link SequenceHeader} parsed from the input OBU, or {@code null} if the AV1
* bitstream is not yet supported.
@ -153,38 +198,60 @@ public final class ObuParser {
/** Parses a {@link #OBU_SEQUENCE_HEADER} and creates an instance. */
private SequenceHeader(Obu obu) throws NotYetImplementedException {
int seqLevelIdx0 = 0;
int seqTier0 = 0;
int initialDisplayDelayMinus1 = 0;
checkArgument(obu.type == OBU_SEQUENCE_HEADER);
byte[] data = new byte[obu.payload.remaining()];
// Do not modify obu.payload while reading it.
obu.payload.asReadOnlyBuffer().get(data);
ParsableBitArray obuData = new ParsableBitArray(data);
obuData.skipBits(4); // seq_profile and still_picture
seqProfile = obuData.readBits(3);
obuData.skipBit(); // still_picture
reducedStillPictureHeader = obuData.readBit();
throwWhenFeatureRequired(reducedStillPictureHeader);
boolean timingInfoPresentFlag = obuData.readBit();
if (timingInfoPresentFlag) {
skipTimingInfo(obuData);
decoderModelInfoPresentFlag = obuData.readBit();
if (decoderModelInfoPresentFlag) {
// skip decoder_model_info()
obuData.skipBits(47);
}
} else {
if (reducedStillPictureHeader) {
seqLevelIdx0 = obuData.readBits(5);
decoderModelInfoPresentFlag = false;
}
boolean initialDisplayDelayPresentFlag = obuData.readBit();
int operatingPointsCntMinus1 = obuData.readBits(5);
for (int i = 0; i <= operatingPointsCntMinus1; i++) {
obuData.skipBits(12); // operating_point_idc[ i ]
int seqLevelIdx = obuData.readBits(5);
if (seqLevelIdx > 7) {
obuData.skipBit(); // seq_tier[ i ]
initialDisplayDelayPresentFlag = false;
} else {
boolean timingInfoPresentFlag = obuData.readBit();
if (timingInfoPresentFlag) {
skipTimingInfo(obuData);
decoderModelInfoPresentFlag = obuData.readBit();
if (decoderModelInfoPresentFlag) {
// skip decoder_model_info()
obuData.skipBits(47);
}
} else {
decoderModelInfoPresentFlag = false;
}
throwWhenFeatureRequired(decoderModelInfoPresentFlag);
if (initialDisplayDelayPresentFlag) {
boolean initialDisplayDelayPresentForThisOpFlag = obuData.readBit();
if (initialDisplayDelayPresentForThisOpFlag) {
obuData.skipBits(4); // initial_display_delay_minus_1[ i ]
initialDisplayDelayPresentFlag = obuData.readBit();
int operatingPointsCntMinus1 = obuData.readBits(5);
for (int i = 0; i <= operatingPointsCntMinus1; i++) {
obuData.skipBits(12); // operating_point_idc[ i ]
if (i == 0) {
seqLevelIdx0 = obuData.readBits(5);
if (seqLevelIdx0 > 7) {
seqTier0 = obuData.readBit() ? 1 : 0;
}
} else {
int seqLevelIdx = obuData.readBits(5);
if (seqLevelIdx > 7) {
obuData.skipBit(); // seq_tier[ i ]
}
}
if (decoderModelInfoPresentFlag) {
obuData.skipBit(); // decoder_model_present_for_this_op
}
if (initialDisplayDelayPresentFlag) {
boolean initialDisplayDelayPresentForThisOpFlag = obuData.readBit();
if (initialDisplayDelayPresentForThisOpFlag) {
if (i == 0) {
initialDisplayDelayMinus1 = obuData.readBits(4);
} else {
obuData.skipBits(4); // initial_display_delay_minus_1[ i ]
}
}
}
}
}
@ -192,39 +259,119 @@ public final class ObuParser {
int frameHeightBitsMinus1 = obuData.readBits(4);
obuData.skipBits(frameWidthBitsMinus1 + 1); // max_frame_width_minus_1
obuData.skipBits(frameHeightBitsMinus1 + 1); // max_frame_height_minus_1
frameIdNumbersPresentFlag = obuData.readBit();
throwWhenFeatureRequired(frameIdNumbersPresentFlag);
if (!reducedStillPictureHeader) {
frameIdNumbersPresentFlag = obuData.readBit();
} else {
frameIdNumbersPresentFlag = false;
}
if (frameIdNumbersPresentFlag) {
obuData.skipBits(4); // delta_frame_id_length_minus_2
obuData.skipBits(3); // additional_frame_id_length_minus_1
}
// use_128x128_superblock, enable_filter_intra, and enable_intra_edge_filter
obuData.skipBits(3);
// enable_interintra_compound, enable_masked_compound, enable_warped_motion, and
// enable_dual_filter
obuData.skipBits(4);
boolean enableOrderHint = obuData.readBit();
if (enableOrderHint) {
obuData.skipBits(2); // enable_jnt_comp and enable_ref_frame_mvs
}
boolean seqChooseScreenContentTools = obuData.readBit();
if (seqChooseScreenContentTools) {
seqForceScreenContentTools = true;
} else {
seqForceScreenContentTools = obuData.readBit();
}
if (seqForceScreenContentTools) {
boolean seqChooseIntegerMv = obuData.readBit();
if (seqChooseIntegerMv) {
seqForceIntegerMv = true;
} else {
seqForceIntegerMv = obuData.readBit();
}
} else {
if (reducedStillPictureHeader) {
seqForceIntegerMv = true;
}
if (enableOrderHint) {
int orderHintBitsMinus1 = obuData.readBits(3);
orderHintBits = orderHintBitsMinus1 + 1;
} else {
seqForceScreenContentTools = true;
orderHintBits = 0;
} else {
// enable_interintra_compound, enable_masked_compound, enable_warped_motion, and
// enable_dual_filter
obuData.skipBits(4);
boolean enableOrderHint = obuData.readBit();
if (enableOrderHint) {
obuData.skipBits(2); // enable_jnt_comp and enable_ref_frame_mvs
}
boolean seqChooseScreenContentTools = obuData.readBit();
if (seqChooseScreenContentTools) {
seqForceScreenContentTools = true;
} else {
seqForceScreenContentTools = obuData.readBit();
}
if (seqForceScreenContentTools) {
boolean seqChooseIntegerMv = obuData.readBit();
if (seqChooseIntegerMv) {
seqForceIntegerMv = true;
} else {
seqForceIntegerMv = obuData.readBit();
}
} else {
seqForceIntegerMv = true;
}
if (enableOrderHint) {
int orderHintBitsMinus1 = obuData.readBits(3);
orderHintBits = orderHintBitsMinus1 + 1;
} else {
orderHintBits = 0;
}
}
this.seqLevelIdx0 = seqLevelIdx0;
this.seqTier0 = seqTier0;
this.initialDisplayDelayMinus1 = initialDisplayDelayMinus1;
// enable_superres, enable_cdef, enable_restoration
obuData.skipBits(3);
// Begin Color Config
highBitdepth = obuData.readBit();
if (seqProfile == 2 && highBitdepth) {
twelveBit = obuData.readBit();
} else {
twelveBit = false;
}
if (seqProfile != 1) {
monochrome = obuData.readBit();
} else {
monochrome = false;
}
boolean colorDescriptionPresent = obuData.readBit();
if (colorDescriptionPresent) {
colorPrimaries = (byte) obuData.readBits(8);
transferCharacteristics = (byte) obuData.readBits(8);
matrixCoefficients = (byte) obuData.readBits(8);
} else {
colorPrimaries = 0;
transferCharacteristics = 0;
matrixCoefficients = 0;
}
if (monochrome) {
obuData.skipBit(); // color_range
subsamplingX = false;
subsamplingY = false;
chromaSamplePosition = 0;
} else if (colorPrimaries == 0x1 /* CP_BT_709 */
&& transferCharacteristics == 13 /* TC_SRGB */
&& matrixCoefficients == 0x0 /* MC_IDENTITY */) {
// Nothing to read from obu.
subsamplingX = false;
subsamplingY = false;
chromaSamplePosition = 0;
} else {
obuData.skipBit(); // color_range
if (seqProfile == 0) {
subsamplingX = true;
subsamplingY = true;
} else if (seqProfile == 1) {
subsamplingX = false;
subsamplingY = false;
} else {
if (twelveBit) {
subsamplingX = obuData.readBit();
if (subsamplingX) {
subsamplingY = obuData.readBit();
} else {
subsamplingY = false;
}
} else {
subsamplingX = true;
subsamplingY = false;
}
}
if (subsamplingX && subsamplingY) {
chromaSamplePosition = obuData.readBits(2);
} else {
chromaSamplePosition = 0;
}
}
obuData.skipBit(); // separate_uv_delta_q
}
/** Advances the bit array by skipping the {@code timing_info()} syntax element. */

View File

@ -87,6 +87,20 @@ public class ObuParserTest {
assertThat(sequenceHeader.seqForceScreenContentTools).isTrue();
assertThat(sequenceHeader.seqForceIntegerMv).isTrue();
assertThat(sequenceHeader.orderHintBits).isEqualTo(7);
assertThat(sequenceHeader.seqProfile).isEqualTo(0);
assertThat(sequenceHeader.seqLevelIdx0).isEqualTo(4);
assertThat(sequenceHeader.seqTier0).isEqualTo(0);
assertThat(sequenceHeader.initialDisplayDelayPresentFlag).isFalse();
assertThat(sequenceHeader.initialDisplayDelayMinus1).isEqualTo(0);
assertThat(sequenceHeader.highBitdepth).isFalse();
assertThat(sequenceHeader.twelveBit).isFalse();
assertThat(sequenceHeader.monochrome).isFalse();
assertThat(sequenceHeader.subsamplingX).isTrue();
assertThat(sequenceHeader.subsamplingY).isTrue();
assertThat(sequenceHeader.chromaSamplePosition).isEqualTo(0);
assertThat(sequenceHeader.colorPrimaries).isEqualTo(1);
assertThat(sequenceHeader.transferCharacteristics).isEqualTo(1);
assertThat(sequenceHeader.matrixCoefficients).isEqualTo(1);
}
@Test

View File

@ -0,0 +1,148 @@
/*
* Copyright (C) 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
*
* http://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.muxer;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.muxer.BoxUtils.concatenateBuffers;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.container.ObuParser;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Utility methods for generating AV1 initialization data.
*
* <p><a href=https://aomediacodec.github.io/av1-spec/>AV1 Bitstream and Decoding Process
* Specification</a>
*/
@UnstableApi
/* package */ final class Av1ConfigUtil {
private static final int MAX_LEB128_SIZE_BYTES = 8;
private static final int MAX_HEADER_AND_LENGTH_SIZE_BYTES = 1 + MAX_LEB128_SIZE_BYTES;
// AV1 configuration record size without config obus.
private static final int MAX_AV1_CONFIG_RECORD_SIZE_BYTES = 4;
// Only the last sample is allowed to have obu_has_size_field == 0.
private static final int OBU_HAS_SIZE_FIELD_BYTES = 1 << 1;
/**
* Generates AV1 initialization data from the first sample {@link ByteBuffer}.
*
* @param byteBuffer The first sample data, in the format specified by the <a
* href=https://aomediacodec.github.io/av1-isobmff/#sampleformat>AV1 Codec ISO Media File
* Format Binding</a>.
* @return The initialization data.
*/
public static byte[] createAv1CodecConfigurationRecord(ByteBuffer byteBuffer) {
@Nullable ByteBuffer csdHeader = null;
@Nullable ByteBuffer configSequenceObu = null;
List<ByteBuffer> configMetadataObusList = new ArrayList<>();
List<ObuParser.Obu> obus = ObuParser.split(byteBuffer);
for (ObuParser.Obu obu : obus) {
if (obu.type == ObuParser.OBU_METADATA) {
configMetadataObusList.add(getConfigObuWithHeaderAndLength(obu));
} else if (obu.type == ObuParser.OBU_SEQUENCE_HEADER && configSequenceObu == null) {
configSequenceObu = getConfigObuWithHeaderAndLength(obu);
csdHeader = parseConfigFromSeqHeader(obu);
}
}
checkNotNull(configSequenceObu, "No sequence header available.");
ByteBuffer configMetadataObus =
concatenateBuffers(configMetadataObusList.toArray(new ByteBuffer[0]));
ByteBuffer configObus = configSequenceObu;
if (configMetadataObus != null) {
configObus = concatenateBuffers(configObus, configMetadataObus);
}
return concatenateBuffers(checkNotNull(csdHeader, "csdHeader is null."), configObus).array();
}
private static ByteBuffer getConfigObuWithHeaderAndLength(ObuParser.Obu obu) {
ByteBuffer configObu =
ByteBuffer.allocate(MAX_HEADER_AND_LENGTH_SIZE_BYTES + obu.payload.remaining());
configObu.put(obuHeader(obu.type));
configObu.put(lebEncode(obu.payload.remaining()));
configObu.put(obu.payload.duplicate());
configObu.flip();
return configObu;
}
// Obu header byte - https://aomediacodec.github.io/av1-spec/#obu-header-syntax.
private static byte obuHeader(int obuType) {
return (byte) (obuType << 3 | OBU_HAS_SIZE_FIELD_BYTES);
}
private static ByteBuffer lebEncode(int value) {
checkArgument(value > 0);
int lebSize = lebSizeInBytes(value);
ByteBuffer sizeBytes = ByteBuffer.allocate(lebSize);
checkState(lebSize < MAX_LEB128_SIZE_BYTES);
for (int i = 0; i < lebSize; ++i) {
int byteValue = (byte) (value & 0x7f);
value >>= 7;
if (value != 0) {
byteValue |= 0x80; // signal that more bytes follow.
}
sizeBytes.put((byte) byteValue);
}
sizeBytes.flip();
return sizeBytes;
}
private static int lebSizeInBytes(int value) {
int size = 0;
do {
size++;
value >>= 7;
} while (value != 0);
return size;
}
private static ByteBuffer parseConfigFromSeqHeader(ObuParser.Obu obu) {
ByteBuffer csd = ByteBuffer.allocate(MAX_AV1_CONFIG_RECORD_SIZE_BYTES);
csd.put((byte) (0x1 << 7 | 0x1)); // Marker(0x1) << 7 | Version (0x1)
@Nullable ObuParser.SequenceHeader sequenceHeader = ObuParser.SequenceHeader.parse(obu);
checkNotNull(sequenceHeader, "No sequence header available.");
csd.put((byte) (sequenceHeader.seqProfile << 5 | sequenceHeader.seqLevelIdx0));
csd.put(
(byte)
((sequenceHeader.seqTier0 > 0 ? 0x80 : 0x0)
| (sequenceHeader.highBitdepth ? 0x40 : 0x0)
| (sequenceHeader.twelveBit ? 0x20 : 0x0)
| (sequenceHeader.monochrome ? 0x10 : 0x0)
| (sequenceHeader.subsamplingX ? 0x08 : 0x0)
| (sequenceHeader.subsamplingY ? 0x04 : 0x0)
| sequenceHeader.chromaSamplePosition));
csd.put(
(byte)
((sequenceHeader.initialDisplayDelayPresentFlag ? 0x10 : 0x0)
| (sequenceHeader.initialDisplayDelayPresentFlag
? sequenceHeader.initialDisplayDelayMinus1 & 0x0F
: 0x0)));
csd.flip();
return csd;
}
private Av1ConfigUtil() {}
}

View File

@ -50,6 +50,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import org.checkerframework.checker.nullness.qual.PolyNull;
/** Writes out various types of boxes as per MP4 (ISO/IEC 14496-12) standards. */
@ -145,6 +146,14 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
continue;
}
Format format = track.format;
if (Objects.equals(track.format.sampleMimeType, MimeTypes.VIDEO_AV1)
&& format.initializationData.isEmpty()) {
format =
format
.buildUpon()
.setInitializationData(ImmutableList.of(checkNotNull(track.parsedCsd)))
.build();
}
String languageCode = bcp47LanguageTagToIso3(format.language);
// Generate the sample durations to calculate the total duration for tkhd box.
@ -1533,11 +1542,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
/** Returns the av1C box. */
private static ByteBuffer av1CBox(Format format) {
// For AV1, the entire codec-specific box is packed into csd-0.
checkArgument(
!format.initializationData.isEmpty(), "csd-0 is not found in the format for av1C box");
byte[] csd0 = format.initializationData.get(0);
checkArgument(csd0.length > 0, "csd-0 is empty for av1C box.");
return BoxUtils.wrapIntoBox("av1C", ByteBuffer.wrap(csd0));
}

View File

@ -19,6 +19,7 @@ import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.muxer.AnnexBUtils.doesSampleContainAnnexBNalUnits;
import static androidx.media3.muxer.Av1ConfigUtil.createAv1CodecConfigurationRecord;
import static androidx.media3.muxer.Boxes.BOX_HEADER_SIZE;
import static androidx.media3.muxer.Boxes.MFHD_BOX_CONTENT_SIZE;
import static androidx.media3.muxer.Boxes.TFHD_BOX_CONTENT_SIZE;
@ -40,6 +41,7 @@ import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
@ -165,6 +167,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void writeSampleData(Track track, ByteBuffer byteBuffer, BufferInfo bufferInfo)
throws IOException {
if (Objects.equals(track.format.sampleMimeType, MimeTypes.VIDEO_AV1)
&& track.format.initializationData.isEmpty()
&& track.parsedCsd == null) {
track.parsedCsd = createAv1CodecConfigurationRecord(byteBuffer.duplicate());
}
if (!headerCreated) {
createHeader();
headerCreated = true;

View File

@ -18,6 +18,7 @@ package androidx.media3.muxer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.muxer.AnnexBUtils.doesSampleContainAnnexBNalUnits;
import static androidx.media3.muxer.Av1ConfigUtil.createAv1CodecConfigurationRecord;
import static androidx.media3.muxer.Boxes.BOX_HEADER_SIZE;
import static androidx.media3.muxer.Boxes.LARGE_SIZE_BOX_HEADER_SIZE;
import static androidx.media3.muxer.Boxes.getAxteBoxHeader;
@ -28,6 +29,7 @@ import static java.lang.Math.max;
import static java.lang.Math.min;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util;
import androidx.media3.container.MdtaMetadataEntry;
import com.google.common.collect.Range;
@ -37,6 +39,7 @@ import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/** Writes all media samples into a single mdat box. */
@ -151,6 +154,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/
public void writeSampleData(Track track, ByteBuffer byteBuffer, BufferInfo bufferInfo)
throws IOException {
if (Objects.equals(track.format.sampleMimeType, MimeTypes.VIDEO_AV1)
&& track.format.initializationData.isEmpty()
&& track.parsedCsd == null) {
track.parsedCsd = createAv1CodecConfigurationRecord(byteBuffer.duplicate());
}
track.writeSampleData(byteBuffer, bufferInfo);
if (sampleBatchingEnabled) {
doInterleave();

View File

@ -17,6 +17,7 @@ package androidx.media3.muxer;
import static androidx.media3.common.util.Assertions.checkArgument;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
@ -37,6 +38,7 @@ import java.util.List;
public final Deque<BufferInfo> pendingSamplesBufferInfo;
public final Deque<ByteBuffer> pendingSamplesByteBuffer;
public boolean hadKeyframe;
@Nullable public byte[] parsedCsd;
public long endOfStreamTimestampUs;
private final boolean sampleCopyEnabled;

View File

@ -0,0 +1,79 @@
/*
* 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.muxer;
import static androidx.media3.muxer.Av1ConfigUtil.createAv1CodecConfigurationRecord;
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 Av1ConfigUtil} */
@RunWith(AndroidJUnit4.class)
public class Av1ConfigUtilTest {
private static final ByteBuffer SEQUENCE_HEADER_AND_METADATA =
ByteBuffer.wrap(
createByteArray(
0x0A, 0x0E, 0x00, 0x00, 0x00, 0x24, 0xC6, 0xAB, 0xDF, 0x3E, 0xFE, 0x24, 0x04, 0x04,
0x04, 0x10, 0x2A, 0x11, 0x0E, 0x10, 0x00, 0xC8, 0xC6, 0x00, 0x00, 0x0C, 0x00, 0x00,
0x00, 0x12, 0x03, 0xCE, 0x0A, 0x50, 0x24));
private static final ByteBuffer METADATA_AND_SEQUENCE_HEADER =
ByteBuffer.wrap(
createByteArray(
0x2A, 0x11, 0x0E, 0x10, 0x00, 0xC8, 0xC6, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x12,
0x03, 0xCE, 0x0A, 0x50, 0x24, 0x0A, 0x0E, 0x00, 0x00, 0x00, 0x24, 0xC6, 0xAB, 0xDF,
0x3E, 0xFE, 0x24, 0x04, 0x04, 0x04, 0x10));
private static final ByteBuffer SEQUENCE_HEADER_YUV444 =
ByteBuffer.wrap(
createByteArray(
0x0A, 0x0B, 0x20, 0x00, 0x00, 0x2D, 0x3D, 0xAB, 0xDF, 0x3E, 0xFE, 0x24, 0x04));
@Test
public void createAv1ConfigRecord_withSequenceHeaderAndMetaData_matchesExpected() {
byte[] initializationData = createAv1CodecConfigurationRecord(SEQUENCE_HEADER_AND_METADATA);
byte[] expectedInitializationData = {
-127, 4, 12, 0, 10, 14, 0, 0, 0, 36, -58, -85, -33, 62, -2, 36, 4, 4, 4, 16, 42, 17, 14, 16,
0, -56, -58, 0, 0, 12, 0, 0, 0, 18, 3, -50, 10, 80, 36
};
assertThat(initializationData).isEqualTo(expectedInitializationData);
}
@Test
public void createAv1ConfigRecord_withMetaDataAndSequenceHeader_matchesExpected() {
byte[] initializationData = createAv1CodecConfigurationRecord(METADATA_AND_SEQUENCE_HEADER);
byte[] expectedInitializationData = {
-127, 4, 12, 0, 10, 14, 0, 0, 0, 36, -58, -85, -33, 62, -2, 36, 4, 4, 4, 16, 42, 17, 14, 16,
0, -56, -58, 0, 0, 12, 0, 0, 0, 18, 3, -50, 10, 80, 36
};
assertThat(initializationData).isEqualTo(expectedInitializationData);
}
@Test
public void createAv1ConfigRecord_withSequenceHeaderYuv444_matchesExpected() {
byte[] initializationData = createAv1CodecConfigurationRecord(SEQUENCE_HEADER_YUV444);
byte[] expectedInitializationData = {
-127, 37, 0, 0, 10, 11, 32, 0, 0, 45, 61, -85, -33, 62, -2, 36, 4
};
assertThat(initializationData).isEqualTo(expectedInitializationData);
}
}

View File

@ -22,10 +22,13 @@ import static androidx.media3.muxer.MuxerTestUtil.MP4_FILE_ASSET_DIRECTORY;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.MediaFormatUtil;
import androidx.media3.container.Mp4TimestampData;
import androidx.media3.exoplayer.MediaExtractorCompat;
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
import androidx.media3.test.utils.DumpFileAsserts;
import androidx.media3.test.utils.DumpableMp4Box;
import androidx.media3.test.utils.FakeExtractorOutput;
@ -199,17 +202,57 @@ public class FragmentedMp4MuxerEndToEndTest {
MuxerTestUtil.getExpectedDumpFilePath(AUDIO_ONLY_MP4 + "_fragmented_box_structure"));
}
@Test
public void createAv1FragmentedMp4File_withoutCsd_matchesExpected() throws Exception {
String outputFilePath = temporaryFolder.newFile().getPath();
FragmentedMp4Muxer mp4Muxer =
new FragmentedMp4Muxer.Builder(new FileOutputStream(outputFilePath)).build();
try {
mp4Muxer.addMetadataEntry(
new Mp4TimestampData(
/* creationTimestampSeconds= */ 100_000_000L,
/* modificationTimestampSeconds= */ 500_000_000L));
feedInputDataToMuxer(context, mp4Muxer, AV1_MP4, /* removeInitializationData= */ true);
} finally {
if (mp4Muxer != null) {
mp4Muxer.close();
}
}
FakeExtractorOutput fakeExtractorOutput =
TestUtil.extractAllSamplesFromFilePath(
new FragmentedMp4Extractor(new DefaultSubtitleParserFactory()),
checkNotNull(outputFilePath));
DumpFileAsserts.assertOutput(
context,
fakeExtractorOutput,
MuxerTestUtil.getExpectedDumpFilePath(AV1_MP4 + "_fragmented"));
}
private static void feedInputDataToMuxer(
Context context, FragmentedMp4Muxer muxer, String inputFileName)
throws IOException, MuxerException {
feedInputDataToMuxer(context, muxer, inputFileName, /* removeInitializationData= */ false);
}
private static void feedInputDataToMuxer(
Context context,
FragmentedMp4Muxer muxer,
String inputFileName,
boolean removeInitializationData)
throws IOException, MuxerException {
MediaExtractorCompat extractor = new MediaExtractorCompat(context);
Uri fileUri = Uri.parse(MP4_FILE_ASSET_DIRECTORY + inputFileName);
extractor.setDataSource(fileUri, /* offset= */ 0);
List<Integer> addedTracks = new ArrayList<>();
for (int i = 0; i < extractor.getTrackCount(); i++) {
int trackId =
muxer.addTrack(MediaFormatUtil.createFormatFromMediaFormat(extractor.getTrackFormat(i)));
Format format = MediaFormatUtil.createFormatFromMediaFormat(extractor.getTrackFormat(i));
if (removeInitializationData && MimeTypes.isVideo(format.sampleMimeType)) {
format = format.buildUpon().setInitializationData(null).build();
}
int trackId = muxer.addTrack(format);
addedTracks.add(trackId);
extractor.selectTrack(i);
}

View File

@ -54,6 +54,7 @@ public class Mp4MuxerEndToEndTest {
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
private static final String H265_HDR10_MP4 = "hdr10-720p.mp4";
private static final String AV1_MP4 = "sample_av1.mp4";
private final Context context = ApplicationProvider.getApplicationContext();
@Test
@ -118,6 +119,30 @@ public class Mp4MuxerEndToEndTest {
assertThat(outputFileBytes).isEmpty();
}
@Test
public void createAv1Mp4File_withoutCsd_matchesExpected() throws Exception {
String outputFilePath = temporaryFolder.newFile().getPath();
Mp4Muxer mp4Muxer = new Mp4Muxer.Builder(new FileOutputStream(outputFilePath)).build();
try {
mp4Muxer.addMetadataEntry(
new Mp4TimestampData(
/* creationTimestampSeconds= */ 100_000_000L,
/* modificationTimestampSeconds= */ 500_000_000L));
feedInputDataToMp4Muxer(context, mp4Muxer, AV1_MP4, /* removeInitializationData= */ true);
} finally {
if (mp4Muxer != null) {
mp4Muxer.close();
}
}
FakeExtractorOutput fakeExtractorOutput =
TestUtil.extractAllSamplesFromFilePath(
new Mp4Extractor(new DefaultSubtitleParserFactory()), checkNotNull(outputFilePath));
DumpFileAsserts.assertOutput(
context, fakeExtractorOutput, MuxerTestUtil.getExpectedDumpFilePath(AV1_MP4));
}
@Test
public void createMp4File_muxerNotClosed_createsPartiallyWrittenValidFile() throws Exception {
String outputPath = temporaryFolder.newFile().getPath();

View File

@ -24,6 +24,7 @@ import android.net.Uri;
import android.util.Pair;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.MediaFormatUtil;
import androidx.media3.exoplayer.MediaExtractorCompat;
import com.google.common.collect.ImmutableList;
@ -81,14 +82,23 @@ import java.util.List;
public static void feedInputDataToMp4Muxer(Context context, Mp4Muxer muxer, String inputFileName)
throws IOException, MuxerException {
feedInputDataToMp4Muxer(context, muxer, inputFileName, /* removeInitializationData= */ false);
}
public static void feedInputDataToMp4Muxer(
Context context, Mp4Muxer muxer, String inputFileName, boolean removeInitializationData)
throws IOException, MuxerException {
MediaExtractorCompat extractor = new MediaExtractorCompat(context);
Uri fileUri = Uri.parse(MP4_FILE_ASSET_DIRECTORY + inputFileName);
extractor.setDataSource(fileUri, /* offset= */ 0);
List<Integer> addedTracks = new ArrayList<>();
for (int i = 0; i < extractor.getTrackCount(); i++) {
int trackId =
muxer.addTrack(MediaFormatUtil.createFormatFromMediaFormat(extractor.getTrackFormat(i)));
Format format = MediaFormatUtil.createFormatFromMediaFormat(extractor.getTrackFormat(i));
if (removeInitializationData && MimeTypes.isVideo(format.sampleMimeType)) {
format = format.buildUpon().setInitializationData(null).build();
}
int trackId = muxer.addTrack(format);
addedTracks.add(trackId);
extractor.selectTrack(i);
}