mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
fe8163838e
commit
641434ff31
@ -43,6 +43,9 @@ public final class ObuParser {
|
|||||||
/** OBU type frame header. */
|
/** OBU type frame header. */
|
||||||
public static final int OBU_FRAME_HEADER = 3;
|
public static final int OBU_FRAME_HEADER = 3;
|
||||||
|
|
||||||
|
/** OBU type metadata. */
|
||||||
|
public static final int OBU_METADATA = 5;
|
||||||
|
|
||||||
/** OBU type frame. */
|
/** OBU type frame. */
|
||||||
public static final int OBU_FRAME = 6;
|
public static final int OBU_FRAME = 6;
|
||||||
|
|
||||||
@ -136,6 +139,48 @@ public final class ObuParser {
|
|||||||
/** See {@code OrderHintBits}. */
|
/** See {@code OrderHintBits}. */
|
||||||
public final int 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
|
* Returns a {@link SequenceHeader} parsed from the input OBU, or {@code null} if the AV1
|
||||||
* bitstream is not yet supported.
|
* bitstream is not yet supported.
|
||||||
@ -153,14 +198,22 @@ public final class ObuParser {
|
|||||||
|
|
||||||
/** Parses a {@link #OBU_SEQUENCE_HEADER} and creates an instance. */
|
/** Parses a {@link #OBU_SEQUENCE_HEADER} and creates an instance. */
|
||||||
private SequenceHeader(Obu obu) throws NotYetImplementedException {
|
private SequenceHeader(Obu obu) throws NotYetImplementedException {
|
||||||
|
int seqLevelIdx0 = 0;
|
||||||
|
int seqTier0 = 0;
|
||||||
|
int initialDisplayDelayMinus1 = 0;
|
||||||
checkArgument(obu.type == OBU_SEQUENCE_HEADER);
|
checkArgument(obu.type == OBU_SEQUENCE_HEADER);
|
||||||
byte[] data = new byte[obu.payload.remaining()];
|
byte[] data = new byte[obu.payload.remaining()];
|
||||||
// Do not modify obu.payload while reading it.
|
// Do not modify obu.payload while reading it.
|
||||||
obu.payload.asReadOnlyBuffer().get(data);
|
obu.payload.asReadOnlyBuffer().get(data);
|
||||||
ParsableBitArray obuData = new ParsableBitArray(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();
|
reducedStillPictureHeader = obuData.readBit();
|
||||||
throwWhenFeatureRequired(reducedStillPictureHeader);
|
if (reducedStillPictureHeader) {
|
||||||
|
seqLevelIdx0 = obuData.readBits(5);
|
||||||
|
decoderModelInfoPresentFlag = false;
|
||||||
|
initialDisplayDelayPresentFlag = false;
|
||||||
|
} else {
|
||||||
boolean timingInfoPresentFlag = obuData.readBit();
|
boolean timingInfoPresentFlag = obuData.readBit();
|
||||||
if (timingInfoPresentFlag) {
|
if (timingInfoPresentFlag) {
|
||||||
skipTimingInfo(obuData);
|
skipTimingInfo(obuData);
|
||||||
@ -172,30 +225,56 @@ public final class ObuParser {
|
|||||||
} else {
|
} else {
|
||||||
decoderModelInfoPresentFlag = false;
|
decoderModelInfoPresentFlag = false;
|
||||||
}
|
}
|
||||||
boolean initialDisplayDelayPresentFlag = obuData.readBit();
|
initialDisplayDelayPresentFlag = obuData.readBit();
|
||||||
int operatingPointsCntMinus1 = obuData.readBits(5);
|
int operatingPointsCntMinus1 = obuData.readBits(5);
|
||||||
for (int i = 0; i <= operatingPointsCntMinus1; i++) {
|
for (int i = 0; i <= operatingPointsCntMinus1; i++) {
|
||||||
obuData.skipBits(12); // operating_point_idc[ 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);
|
int seqLevelIdx = obuData.readBits(5);
|
||||||
if (seqLevelIdx > 7) {
|
if (seqLevelIdx > 7) {
|
||||||
obuData.skipBit(); // seq_tier[ i ]
|
obuData.skipBit(); // seq_tier[ i ]
|
||||||
}
|
}
|
||||||
throwWhenFeatureRequired(decoderModelInfoPresentFlag);
|
}
|
||||||
|
if (decoderModelInfoPresentFlag) {
|
||||||
|
obuData.skipBit(); // decoder_model_present_for_this_op
|
||||||
|
}
|
||||||
if (initialDisplayDelayPresentFlag) {
|
if (initialDisplayDelayPresentFlag) {
|
||||||
boolean initialDisplayDelayPresentForThisOpFlag = obuData.readBit();
|
boolean initialDisplayDelayPresentForThisOpFlag = obuData.readBit();
|
||||||
if (initialDisplayDelayPresentForThisOpFlag) {
|
if (initialDisplayDelayPresentForThisOpFlag) {
|
||||||
|
if (i == 0) {
|
||||||
|
initialDisplayDelayMinus1 = obuData.readBits(4);
|
||||||
|
} else {
|
||||||
obuData.skipBits(4); // initial_display_delay_minus_1[ i ]
|
obuData.skipBits(4); // initial_display_delay_minus_1[ i ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
int frameWidthBitsMinus1 = obuData.readBits(4);
|
int frameWidthBitsMinus1 = obuData.readBits(4);
|
||||||
int frameHeightBitsMinus1 = obuData.readBits(4);
|
int frameHeightBitsMinus1 = obuData.readBits(4);
|
||||||
obuData.skipBits(frameWidthBitsMinus1 + 1); // max_frame_width_minus_1
|
obuData.skipBits(frameWidthBitsMinus1 + 1); // max_frame_width_minus_1
|
||||||
obuData.skipBits(frameHeightBitsMinus1 + 1); // max_frame_height_minus_1
|
obuData.skipBits(frameHeightBitsMinus1 + 1); // max_frame_height_minus_1
|
||||||
|
if (!reducedStillPictureHeader) {
|
||||||
frameIdNumbersPresentFlag = obuData.readBit();
|
frameIdNumbersPresentFlag = obuData.readBit();
|
||||||
throwWhenFeatureRequired(frameIdNumbersPresentFlag);
|
} 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
|
// use_128x128_superblock, enable_filter_intra, and enable_intra_edge_filter
|
||||||
obuData.skipBits(3);
|
obuData.skipBits(3);
|
||||||
|
if (reducedStillPictureHeader) {
|
||||||
|
seqForceIntegerMv = true;
|
||||||
|
seqForceScreenContentTools = true;
|
||||||
|
orderHintBits = 0;
|
||||||
|
} else {
|
||||||
// enable_interintra_compound, enable_masked_compound, enable_warped_motion, and
|
// enable_interintra_compound, enable_masked_compound, enable_warped_motion, and
|
||||||
// enable_dual_filter
|
// enable_dual_filter
|
||||||
obuData.skipBits(4);
|
obuData.skipBits(4);
|
||||||
@ -226,6 +305,74 @@ public final class ObuParser {
|
|||||||
orderHintBits = 0;
|
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. */
|
/** Advances the bit array by skipping the {@code timing_info()} syntax element. */
|
||||||
private static void skipTimingInfo(ParsableBitArray parsableBitArray) {
|
private static void skipTimingInfo(ParsableBitArray parsableBitArray) {
|
||||||
|
@ -87,6 +87,20 @@ public class ObuParserTest {
|
|||||||
assertThat(sequenceHeader.seqForceScreenContentTools).isTrue();
|
assertThat(sequenceHeader.seqForceScreenContentTools).isTrue();
|
||||||
assertThat(sequenceHeader.seqForceIntegerMv).isTrue();
|
assertThat(sequenceHeader.seqForceIntegerMv).isTrue();
|
||||||
assertThat(sequenceHeader.orderHintBits).isEqualTo(7);
|
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
|
@Test
|
||||||
|
@ -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() {}
|
||||||
|
}
|
@ -50,6 +50,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
import org.checkerframework.checker.nullness.qual.PolyNull;
|
import org.checkerframework.checker.nullness.qual.PolyNull;
|
||||||
|
|
||||||
/** Writes out various types of boxes as per MP4 (ISO/IEC 14496-12) standards. */
|
/** 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;
|
continue;
|
||||||
}
|
}
|
||||||
Format format = track.format;
|
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);
|
String languageCode = bcp47LanguageTagToIso3(format.language);
|
||||||
|
|
||||||
// Generate the sample durations to calculate the total duration for tkhd box.
|
// 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. */
|
/** Returns the av1C box. */
|
||||||
private static ByteBuffer av1CBox(Format format) {
|
private static ByteBuffer av1CBox(Format format) {
|
||||||
// For AV1, the entire codec-specific box is packed into csd-0.
|
// 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);
|
byte[] csd0 = format.initializationData.get(0);
|
||||||
checkArgument(csd0.length > 0, "csd-0 is empty for av1C box.");
|
|
||||||
|
|
||||||
return BoxUtils.wrapIntoBox("av1C", ByteBuffer.wrap(csd0));
|
return BoxUtils.wrapIntoBox("av1C", ByteBuffer.wrap(csd0));
|
||||||
}
|
}
|
||||||
|
@ -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.checkNotNull;
|
||||||
import static androidx.media3.common.util.Assertions.checkState;
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
import static androidx.media3.muxer.AnnexBUtils.doesSampleContainAnnexBNalUnits;
|
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.BOX_HEADER_SIZE;
|
||||||
import static androidx.media3.muxer.Boxes.MFHD_BOX_CONTENT_SIZE;
|
import static androidx.media3.muxer.Boxes.MFHD_BOX_CONTENT_SIZE;
|
||||||
import static androidx.media3.muxer.Boxes.TFHD_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.nio.channels.WritableByteChannel;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
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)
|
public void writeSampleData(Track track, ByteBuffer byteBuffer, BufferInfo bufferInfo)
|
||||||
throws IOException {
|
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) {
|
if (!headerCreated) {
|
||||||
createHeader();
|
createHeader();
|
||||||
headerCreated = true;
|
headerCreated = true;
|
||||||
|
@ -18,6 +18,7 @@ package androidx.media3.muxer;
|
|||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.common.util.Assertions.checkState;
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
import static androidx.media3.muxer.AnnexBUtils.doesSampleContainAnnexBNalUnits;
|
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.BOX_HEADER_SIZE;
|
||||||
import static androidx.media3.muxer.Boxes.LARGE_SIZE_BOX_HEADER_SIZE;
|
import static androidx.media3.muxer.Boxes.LARGE_SIZE_BOX_HEADER_SIZE;
|
||||||
import static androidx.media3.muxer.Boxes.getAxteBoxHeader;
|
import static androidx.media3.muxer.Boxes.getAxteBoxHeader;
|
||||||
@ -28,6 +29,7 @@ import static java.lang.Math.max;
|
|||||||
import static java.lang.Math.min;
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.container.MdtaMetadataEntry;
|
import androidx.media3.container.MdtaMetadataEntry;
|
||||||
import com.google.common.collect.Range;
|
import com.google.common.collect.Range;
|
||||||
@ -37,6 +39,7 @@ import java.nio.channels.FileChannel;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/** Writes all media samples into a single mdat box. */
|
/** 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)
|
public void writeSampleData(Track track, ByteBuffer byteBuffer, BufferInfo bufferInfo)
|
||||||
throws IOException {
|
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);
|
track.writeSampleData(byteBuffer, bufferInfo);
|
||||||
if (sampleBatchingEnabled) {
|
if (sampleBatchingEnabled) {
|
||||||
doInterleave();
|
doInterleave();
|
||||||
|
@ -17,6 +17,7 @@ package androidx.media3.muxer;
|
|||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
@ -37,6 +38,7 @@ import java.util.List;
|
|||||||
public final Deque<BufferInfo> pendingSamplesBufferInfo;
|
public final Deque<BufferInfo> pendingSamplesBufferInfo;
|
||||||
public final Deque<ByteBuffer> pendingSamplesByteBuffer;
|
public final Deque<ByteBuffer> pendingSamplesByteBuffer;
|
||||||
public boolean hadKeyframe;
|
public boolean hadKeyframe;
|
||||||
|
@Nullable public byte[] parsedCsd;
|
||||||
public long endOfStreamTimestampUs;
|
public long endOfStreamTimestampUs;
|
||||||
|
|
||||||
private final boolean sampleCopyEnabled;
|
private final boolean sampleCopyEnabled;
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -22,10 +22,13 @@ import static androidx.media3.muxer.MuxerTestUtil.MP4_FILE_ASSET_DIRECTORY;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.Format;
|
||||||
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.MediaFormatUtil;
|
import androidx.media3.common.util.MediaFormatUtil;
|
||||||
import androidx.media3.container.Mp4TimestampData;
|
import androidx.media3.container.Mp4TimestampData;
|
||||||
import androidx.media3.exoplayer.MediaExtractorCompat;
|
import androidx.media3.exoplayer.MediaExtractorCompat;
|
||||||
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
||||||
|
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
|
||||||
import androidx.media3.test.utils.DumpFileAsserts;
|
import androidx.media3.test.utils.DumpFileAsserts;
|
||||||
import androidx.media3.test.utils.DumpableMp4Box;
|
import androidx.media3.test.utils.DumpableMp4Box;
|
||||||
import androidx.media3.test.utils.FakeExtractorOutput;
|
import androidx.media3.test.utils.FakeExtractorOutput;
|
||||||
@ -199,17 +202,57 @@ public class FragmentedMp4MuxerEndToEndTest {
|
|||||||
MuxerTestUtil.getExpectedDumpFilePath(AUDIO_ONLY_MP4 + "_fragmented_box_structure"));
|
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(
|
private static void feedInputDataToMuxer(
|
||||||
Context context, FragmentedMp4Muxer muxer, String inputFileName)
|
Context context, FragmentedMp4Muxer muxer, String inputFileName)
|
||||||
throws IOException, MuxerException {
|
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);
|
MediaExtractorCompat extractor = new MediaExtractorCompat(context);
|
||||||
Uri fileUri = Uri.parse(MP4_FILE_ASSET_DIRECTORY + inputFileName);
|
Uri fileUri = Uri.parse(MP4_FILE_ASSET_DIRECTORY + inputFileName);
|
||||||
extractor.setDataSource(fileUri, /* offset= */ 0);
|
extractor.setDataSource(fileUri, /* offset= */ 0);
|
||||||
|
|
||||||
List<Integer> addedTracks = new ArrayList<>();
|
List<Integer> addedTracks = new ArrayList<>();
|
||||||
for (int i = 0; i < extractor.getTrackCount(); i++) {
|
for (int i = 0; i < extractor.getTrackCount(); i++) {
|
||||||
int trackId =
|
Format format = MediaFormatUtil.createFormatFromMediaFormat(extractor.getTrackFormat(i));
|
||||||
muxer.addTrack(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);
|
addedTracks.add(trackId);
|
||||||
extractor.selectTrack(i);
|
extractor.selectTrack(i);
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,7 @@ public class Mp4MuxerEndToEndTest {
|
|||||||
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
private static final String H265_HDR10_MP4 = "hdr10-720p.mp4";
|
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();
|
private final Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -118,6 +119,30 @@ public class Mp4MuxerEndToEndTest {
|
|||||||
assertThat(outputFileBytes).isEmpty();
|
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
|
@Test
|
||||||
public void createMp4File_muxerNotClosed_createsPartiallyWrittenValidFile() throws Exception {
|
public void createMp4File_muxerNotClosed_createsPartiallyWrittenValidFile() throws Exception {
|
||||||
String outputPath = temporaryFolder.newFile().getPath();
|
String outputPath = temporaryFolder.newFile().getPath();
|
||||||
|
@ -24,6 +24,7 @@ import android.net.Uri;
|
|||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.MediaFormatUtil;
|
import androidx.media3.common.util.MediaFormatUtil;
|
||||||
import androidx.media3.exoplayer.MediaExtractorCompat;
|
import androidx.media3.exoplayer.MediaExtractorCompat;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
@ -81,14 +82,23 @@ import java.util.List;
|
|||||||
|
|
||||||
public static void feedInputDataToMp4Muxer(Context context, Mp4Muxer muxer, String inputFileName)
|
public static void feedInputDataToMp4Muxer(Context context, Mp4Muxer muxer, String inputFileName)
|
||||||
throws IOException, MuxerException {
|
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);
|
MediaExtractorCompat extractor = new MediaExtractorCompat(context);
|
||||||
Uri fileUri = Uri.parse(MP4_FILE_ASSET_DIRECTORY + inputFileName);
|
Uri fileUri = Uri.parse(MP4_FILE_ASSET_DIRECTORY + inputFileName);
|
||||||
extractor.setDataSource(fileUri, /* offset= */ 0);
|
extractor.setDataSource(fileUri, /* offset= */ 0);
|
||||||
|
|
||||||
List<Integer> addedTracks = new ArrayList<>();
|
List<Integer> addedTracks = new ArrayList<>();
|
||||||
for (int i = 0; i < extractor.getTrackCount(); i++) {
|
for (int i = 0; i < extractor.getTrackCount(); i++) {
|
||||||
int trackId =
|
Format format = MediaFormatUtil.createFormatFromMediaFormat(extractor.getTrackFormat(i));
|
||||||
muxer.addTrack(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);
|
addedTracks.add(trackId);
|
||||||
extractor.selectTrack(i);
|
extractor.selectTrack(i);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user