Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java
Original file line number Diff line number Diff line change
Expand Up @@ -1288,8 +1288,13 @@ public static ByteBuffer stbl(ByteBuffer... subBoxes) {
return BoxUtils.wrapBoxesIntoBox("stbl", Arrays.asList(subBoxes));
}

/** Creates the ftyp box. */
public static ByteBuffer ftyp() {
/**
* Creates the ftyp box.
*
* @param additionalCompatibleBrands Additional compatible brands to declare, beyond the default
* set ({@code isom}, {@code iso2}, {@code mp41}).
*/
/* package */ static ByteBuffer ftyp(List<String> additionalCompatibleBrands) {
List<ByteBuffer> boxBytes = new ArrayList<>();

String majorVersion = "isom";
Expand All @@ -1301,9 +1306,15 @@ public static ByteBuffer ftyp() {
minorBytes.flip();
boxBytes.add(minorBytes);

String[] compatibleBrands = {"isom", "iso2", "mp41"};
List<String> compatibleBrands = new ArrayList<>(Arrays.asList("isom", "iso2", "mp41"));
compatibleBrands.addAll(additionalCompatibleBrands);
for (String compatibleBrand : compatibleBrands) {
boxBytes.add(ByteBuffer.wrap(Util.getUtf8Bytes(compatibleBrand)));
byte[] compatibleBrandBytes = Util.getUtf8Bytes(compatibleBrand);
checkArgument(
compatibleBrandBytes.length == 4,
"Compatible brand must be 4 bytes: %s",
compatibleBrand);
boxBytes.add(ByteBuffer.wrap(compatibleBrandBytes));
}

return BoxUtils.wrapBoxesIntoBox("ftyp", boxBytes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ private static int calculateMoofBoxSize(List<ProcessedTrackInfo> trackInfos) {
}

private void createHeader() throws IOException {
outputChannel.write(Boxes.ftyp());
outputChannel.write(Boxes.ftyp(MuxerUtil.getFtypCompatibleBrands(tracks)));
outputChannel.write(
Boxes.moov(
tracks, metadataCollector, /* isFragmentedMp4= */ true, lastSampleDurationBehavior));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,10 @@ private Mp4Muxer(
* {@inheritDoc}
*
* <p>Tracks can be added at any point before the muxer is closed, even after writing samples to
* other tracks.
* other tracks. Note that a track requiring an additional {@code ftyp} compatible brand (for
* example a {@link MimeTypes#VIDEO_DOLBY_VISION} track, which requires the {@code dby1} brand)
* that is added after samples have already been written to another track will not retroactively
* update the {@code ftyp} box.
*
* <p>The order of tracks remains same in which they are added.
*
Expand All @@ -509,7 +512,10 @@ public int addTrack(Format format) throws MuxerException {
* Adds a track of the given media format.
*
* <p>Tracks can be added at any point before the muxer is closed, even after writing samples to
* other tracks.
* other tracks. Note that a track requiring an additional {@code ftyp} compatible brand (for
* example a {@link MimeTypes#VIDEO_DOLBY_VISION} track, which requires the {@code dby1} brand)
* that is added after samples have already been written to another track will not retroactively
* update the {@code ftyp} box.
*
* <p>The final order of tracks is determined by the provided sort key. Tracks with a lower sort
* key will be written before tracks with a higher sort key. Ordering between tracks with the same
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ private void writeAxteBox() throws IOException {

private ByteBuffer getAxteBox() {
// The axte box will have one ftyp and one moov box.
ByteBuffer ftypBox = Boxes.ftyp();
ByteBuffer ftypBox = Boxes.ftyp(MuxerUtil.getFtypCompatibleBrands(auxiliaryTracks));
MetadataCollector auxiliaryTracksMetadataCollector = new MetadataCollector();
populateAuxiliaryTracksMetadata(
auxiliaryTracksMetadataCollector,
Expand Down Expand Up @@ -314,7 +314,7 @@ public void finalizeMoovBox() throws IOException {

private void writeHeader() throws IOException {
muxerOutput.setPosition(0L);
muxerOutput.write(Boxes.ftyp());
muxerOutput.write(Boxes.ftyp(MuxerUtil.getFtypCompatibleBrands(tracks)));

if (freeSpaceAfterFtypInBytes > 0) {
muxerOutput.write(
Expand Down
31 changes: 31 additions & 0 deletions libraries/muxer/src/main/java/androidx/media3/muxer/MuxerUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import androidx.media3.container.Mp4OrientationData;
import androidx.media3.container.Mp4TimestampData;
import androidx.media3.container.XmpData;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Longs;
import java.io.FileInputStream;
import java.io.IOException;
Expand All @@ -43,6 +45,7 @@
import java.nio.channels.WritableByteChannel;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

/** Utility methods for muxer. */
@UnstableApi
Expand Down Expand Up @@ -147,6 +150,34 @@ public static void createMotionPhotoFromJpegImageAndBmffVideo(
|| format.auxiliaryTrackType == C.AUXILIARY_TRACK_TYPE_DEPTH_METADATA);
}

/**
* Returns the additional ftyp compatible brands that should be declared for the given tracks.
*/
/* package */ static ImmutableList<String> getFtypCompatibleBrands(List<Track> tracks) {
ImmutableSet.Builder<String> compatibleBrands = new ImmutableSet.Builder<>();
for (int i = 0; i < tracks.size(); i++) {
if (isDolbyTrack(tracks.get(i).format)) {
compatibleBrands.add("dby1");
}
}
return compatibleBrands.build().asList();
}

/**
* Returns whether the given {@linkplain Format track format} is a Dolby format (Dolby Vision
* video, or Dolby AC-3, EAC3, EAC3-JOC, AC-4 or TrueHD audio) that requires the {@code dby1}
* compatible brand in the ftyp box.
*/
private static boolean isDolbyTrack(Format format) {
String sampleMimeType = format.sampleMimeType;
return Objects.equals(sampleMimeType, MimeTypes.VIDEO_DOLBY_VISION)
|| Objects.equals(sampleMimeType, MimeTypes.AUDIO_AC3)
|| Objects.equals(sampleMimeType, MimeTypes.AUDIO_AC4)
|| Objects.equals(sampleMimeType, MimeTypes.AUDIO_E_AC3)
|| Objects.equals(sampleMimeType, MimeTypes.AUDIO_E_AC3_JOC)
|| Objects.equals(sampleMimeType, MimeTypes.AUDIO_TRUEHD);
}

/** Returns a {@link MdtaMetadataEntry} for the auxiliary tracks offset metadata. */
/* package */ static MdtaMetadataEntry getAuxiliaryTracksOffsetMetadata(long offset) {
return new MdtaMetadataEntry(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -950,13 +950,44 @@ public void createStssBox_matchesExpected() throws IOException {

@Test
public void createFtypBox_matchesExpected() throws IOException {
ByteBuffer ftypBox = Boxes.ftyp();
ByteBuffer ftypBox = Boxes.ftyp(/* additionalCompatibleBrands= */ ImmutableList.of());

DumpableMp4Box dumpableBox = new DumpableMp4Box(ftypBox);
DumpFileAsserts.assertOutput(
context, dumpableBox, MuxerTestUtil.getExpectedMp4DumpFilePath("ftyp_box"));
}

@Test
public void createFtypBox_withInvalidCompatibleBrandLength_throws() {
assertThrows(
IllegalArgumentException.class,
() -> Boxes.ftyp(/* additionalCompatibleBrands= */ ImmutableList.of("dby")));
}

@Test
public void createFtypBox_forDolbyVision_containsDby1CompatibleBrand() {
ByteBuffer ftypBox = Boxes.ftyp(/* additionalCompatibleBrands= */ ImmutableList.of("dby1"));

assertThat(ftypBox.getInt()).isEqualTo(32);
byte[] type = new byte[4];
ftypBox.get(type);
assertThat(type).isEqualTo(Util.getUtf8Bytes("ftyp"));
assertThat(ftypBox.getInt()).isEqualTo(Util.getIntegerCodeForString("isom"));
assertThat(ftypBox.getInt()).isEqualTo(0x020000);

List<Integer> compatibleBrands = new ArrayList<>();
while (ftypBox.hasRemaining()) {
compatibleBrands.add(ftypBox.getInt());
}
assertThat(compatibleBrands)
.containsExactly(
Util.getIntegerCodeForString("isom"),
Util.getIntegerCodeForString("iso2"),
Util.getIntegerCodeForString("mp41"),
Util.getIntegerCodeForString("dby1"))
.inOrder();
}

@Test
public void createMfhdBox_matchesExpected() throws IOException {
ByteBuffer mfhdBox = Boxes.mfhd(/* sequenceNumber= */ 5);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
*/
package androidx.media3.muxer;

import static androidx.media3.muxer.MuxerTestUtil.FAKE_VIDEO_FORMAT;
import static androidx.media3.muxer.MuxerTestUtil.feedInputDataToMuxer;
import static androidx.media3.muxer.MuxerTestUtil.getFakeSampleAndSampleInfo;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.Truth.assertThat;

import android.content.Context;
import android.util.Pair;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
Expand Down Expand Up @@ -83,6 +86,30 @@ public void createFragmentedMp4File_fromInputFileSampleData_matchesExpectedBoxSt
+ "_fragmented_box_structure"));
}

@Test
public void createFragmentedMp4File_withDolbyVisionTrack_ftypContainsDby1CompatibleBrand()
throws Exception {
String outputFilePath = temporaryFolder.newFile().getPath();
Format dolbyVisionFormat =
FAKE_VIDEO_FORMAT
.buildUpon()
.setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION)
.setCodecs("dvav.09.02")
.build();
Pair<ByteBuffer, BufferInfo> sampleAndSampleInfo =
getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 0L, /* isVideo= */ true);

try (FragmentedMp4Muxer muxer =
new FragmentedMp4Muxer.Builder(new FileOutputStream(outputFilePath).getChannel())
.build()) {
int trackId = muxer.addTrack(dolbyVisionFormat);
muxer.writeSampleData(trackId, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
}

byte[] outputFileBytes = TestUtil.getByteArrayFromFilePath(outputFilePath);
assertThat(MuxerTestUtil.ftypBoxContainsCompatibleBrand(outputFileBytes, "dby1")).isTrue();
}

@Test
public void createFragmentedMp4File_fromAudioOnlyInputFile_writesExpectedFragments()
throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,28 @@ public void writeMp4File_withSampleAndMetadata_matchedExpectedBoxStructure() thr
MuxerTestUtil.getExpectedMp4DumpFilePath("mp4_with_samples_and_metadata.mp4"));
}

@Test
public void createMp4File_withDolbyVisionTrack_ftypContainsDby1CompatibleBrand()
throws Exception {
String outputFilePath = temporaryFolder.newFile().getPath();
Format dolbyVisionFormat =
FAKE_VIDEO_FORMAT
.buildUpon()
.setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION)
.setCodecs("dvav.09.02")
.build();
Pair<ByteBuffer, BufferInfo> sampleAndSampleInfo =
getFakeSampleAndSampleInfo(/* presentationTimeUs= */ 0L, /* isVideo= */ true);

try (Mp4Muxer muxer = new Mp4Muxer.Builder(SeekableMuxerOutput.of(outputFilePath)).build()) {
int trackId = muxer.addTrack(dolbyVisionFormat);
muxer.writeSampleData(trackId, sampleAndSampleInfo.first, sampleAndSampleInfo.second);
}

byte[] outputFileBytes = TestUtil.getByteArrayFromFilePath(outputFilePath);
assertThat(MuxerTestUtil.ftypBoxContainsCompatibleBrand(outputFileBytes, "dby1")).isTrue();
}

@Test
public void createMp4File_addTrackAndMetadataButNoSamples_createsEmptyFile() throws Exception {
String outputFilePath = temporaryFolder.newFile().getPath();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.MediaFormatUtil;
import androidx.media3.common.util.Util;
import androidx.media3.inspector.MediaExtractorCompat;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
Expand Down Expand Up @@ -165,5 +166,23 @@ public static void feedInputDataToMuxer(
extractor.release();
}

/**
* Returns whether the {@code ftyp} box at the start of {@code outputFileBytes} declares {@code
* compatibleBrand} as one of its compatible brands.
*/
public static boolean ftypBoxContainsCompatibleBrand(
byte[] outputFileBytes, String compatibleBrand) {
ByteBuffer buffer = ByteBuffer.wrap(outputFileBytes);
int ftypBoxSize = buffer.getInt();
buffer.position(16); // Skip box size, "ftyp", major_brand and minor_version.
int compatibleBrandCode = Util.getIntegerCodeForString(compatibleBrand);
while (buffer.position() < ftypBoxSize) {
if (buffer.getInt() == compatibleBrandCode) {
return true;
}
}
return false;
}

private MuxerTestUtil() {}
}
Loading