Create FragmentedMp4Muxer class
This CL aims to separate Fragmented MP4 related logic in a separate public class. Earlier all the logic was in a single class `Mp4Muxer`. PiperOrigin-RevId: 619206661
This commit is contained in:
parent
737bf08314
commit
8eb1390f80
@ -15,8 +15,18 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.muxer;
|
package androidx.media3.muxer;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.media.MediaCodec;
|
||||||
|
import android.media.MediaExtractor;
|
||||||
|
import androidx.media3.common.util.MediaFormatUtil;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/** Utilities for muxer test cases. */
|
/** Utilities for muxer test cases. */
|
||||||
/* package */ final class AndroidMuxerTestUtil {
|
/* package */ final class AndroidMuxerTestUtil {
|
||||||
|
private static final String MP4_FILE_ASSET_DIRECTORY = "media/mp4/";
|
||||||
private static final String DUMP_FILE_OUTPUT_DIRECTORY = "muxerdumps";
|
private static final String DUMP_FILE_OUTPUT_DIRECTORY = "muxerdumps";
|
||||||
private static final String DUMP_FILE_EXTENSION = "dump";
|
private static final String DUMP_FILE_EXTENSION = "dump";
|
||||||
|
|
||||||
@ -25,4 +35,38 @@ package androidx.media3.muxer;
|
|||||||
public static String getExpectedDumpFilePath(String originalFileName) {
|
public static String getExpectedDumpFilePath(String originalFileName) {
|
||||||
return DUMP_FILE_OUTPUT_DIRECTORY + '/' + originalFileName + '.' + DUMP_FILE_EXTENSION;
|
return DUMP_FILE_OUTPUT_DIRECTORY + '/' + originalFileName + '.' + DUMP_FILE_EXTENSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void feedInputDataToMuxer(Context context, Muxer muxer, String inputFileName)
|
||||||
|
throws IOException {
|
||||||
|
MediaExtractor extractor = new MediaExtractor();
|
||||||
|
extractor.setDataSource(
|
||||||
|
context.getResources().getAssets().openFd(MP4_FILE_ASSET_DIRECTORY + inputFileName));
|
||||||
|
|
||||||
|
List<Muxer.TrackToken> addedTracks = new ArrayList<>();
|
||||||
|
for (int i = 0; i < extractor.getTrackCount(); i++) {
|
||||||
|
Muxer.TrackToken trackToken =
|
||||||
|
muxer.addTrack(MediaFormatUtil.createFormatFromMediaFormat(extractor.getTrackFormat(i)));
|
||||||
|
addedTracks.add(trackToken);
|
||||||
|
extractor.selectTrack(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||||
|
bufferInfo.flags = extractor.getSampleFlags();
|
||||||
|
bufferInfo.offset = 0;
|
||||||
|
bufferInfo.presentationTimeUs = extractor.getSampleTime();
|
||||||
|
int sampleSize = (int) extractor.getSampleSize();
|
||||||
|
bufferInfo.size = sampleSize;
|
||||||
|
|
||||||
|
ByteBuffer sampleBuffer = ByteBuffer.allocateDirect(sampleSize);
|
||||||
|
extractor.readSampleData(sampleBuffer, /* offset= */ 0);
|
||||||
|
|
||||||
|
sampleBuffer.rewind();
|
||||||
|
|
||||||
|
muxer.writeSampleData(
|
||||||
|
addedTracks.get(extractor.getSampleTrackIndex()), sampleBuffer, bufferInfo);
|
||||||
|
} while (extractor.advance());
|
||||||
|
|
||||||
|
extractor.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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.checkNotNull;
|
||||||
|
import static androidx.media3.muxer.AndroidMuxerTestUtil.feedInputDataToMuxer;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.container.Mp4TimestampData;
|
||||||
|
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
||||||
|
import androidx.media3.test.utils.DumpFileAsserts;
|
||||||
|
import androidx.media3.test.utils.DumpableMp4Box;
|
||||||
|
import androidx.media3.test.utils.FakeExtractorOutput;
|
||||||
|
import androidx.media3.test.utils.TestUtil;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** End to end instrumentation tests for {@link FragmentedMp4Muxer}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class FragmentedMp4MuxerEndToEndAndroidTest {
|
||||||
|
private static final String H265_HDR10_MP4 = "hdr10-720p.mp4";
|
||||||
|
|
||||||
|
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private final Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
private @MonotonicNonNull String outputPath;
|
||||||
|
private @MonotonicNonNull FileOutputStream outputStream;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
outputPath = temporaryFolder.newFile("muxeroutput.mp4").getPath();
|
||||||
|
outputStream = new FileOutputStream(outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() throws IOException {
|
||||||
|
checkNotNull(outputStream).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createFragmentedMp4File_fromInputFileSampleData_matchesExpected() throws IOException {
|
||||||
|
@Nullable Muxer fragmentedMp4Muxer = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fragmentedMp4Muxer = new FragmentedMp4Muxer(checkNotNull(outputStream));
|
||||||
|
fragmentedMp4Muxer.addMetadata(
|
||||||
|
new Mp4TimestampData(
|
||||||
|
/* creationTimestampSeconds= */ 100_000_000L,
|
||||||
|
/* modificationTimestampSeconds= */ 500_000_000L));
|
||||||
|
feedInputDataToMuxer(context, fragmentedMp4Muxer, H265_HDR10_MP4);
|
||||||
|
} finally {
|
||||||
|
if (fragmentedMp4Muxer != null) {
|
||||||
|
fragmentedMp4Muxer.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FakeExtractorOutput fakeExtractorOutput =
|
||||||
|
TestUtil.extractAllSamplesFromFilePath(
|
||||||
|
new FragmentedMp4Extractor(), checkNotNull(outputPath));
|
||||||
|
DumpFileAsserts.assertOutput(
|
||||||
|
context,
|
||||||
|
fakeExtractorOutput,
|
||||||
|
AndroidMuxerTestUtil.getExpectedDumpFilePath(H265_HDR10_MP4 + "_fragmented"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createFragmentedMp4File_fromInputFileSampleData_matchesExpectedBoxStructure()
|
||||||
|
throws IOException {
|
||||||
|
@Nullable Muxer fragmentedMp4Muxer = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fragmentedMp4Muxer = new FragmentedMp4Muxer(checkNotNull(outputStream));
|
||||||
|
fragmentedMp4Muxer.addMetadata(
|
||||||
|
new Mp4TimestampData(
|
||||||
|
/* creationTimestampSeconds= */ 100_000_000L,
|
||||||
|
/* modificationTimestampSeconds= */ 500_000_000L));
|
||||||
|
feedInputDataToMuxer(context, fragmentedMp4Muxer, H265_HDR10_MP4);
|
||||||
|
} finally {
|
||||||
|
if (fragmentedMp4Muxer != null) {
|
||||||
|
fragmentedMp4Muxer.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DumpableMp4Box dumpableMp4Box =
|
||||||
|
new DumpableMp4Box(
|
||||||
|
ByteBuffer.wrap(TestUtil.getByteArrayFromFilePath(checkNotNull(outputPath))));
|
||||||
|
DumpFileAsserts.assertOutput(
|
||||||
|
context,
|
||||||
|
dumpableMp4Box,
|
||||||
|
AndroidMuxerTestUtil.getExpectedDumpFilePath(H265_HDR10_MP4 + "_fragmented_box_structure"));
|
||||||
|
}
|
||||||
|
}
|
@ -16,27 +16,20 @@
|
|||||||
package androidx.media3.muxer;
|
package androidx.media3.muxer;
|
||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
import static androidx.media3.muxer.AndroidMuxerTestUtil.feedInputDataToMuxer;
|
||||||
import static org.junit.Assume.assumeTrue;
|
import static org.junit.Assume.assumeTrue;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.MediaCodec;
|
|
||||||
import android.media.MediaExtractor;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.util.MediaFormatUtil;
|
|
||||||
import androidx.media3.container.Mp4TimestampData;
|
import androidx.media3.container.Mp4TimestampData;
|
||||||
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
|
||||||
import androidx.media3.extractor.mp4.Mp4Extractor;
|
import androidx.media3.extractor.mp4.Mp4Extractor;
|
||||||
import androidx.media3.test.utils.DumpFileAsserts;
|
import androidx.media3.test.utils.DumpFileAsserts;
|
||||||
import androidx.media3.test.utils.DumpableMp4Box;
|
|
||||||
import androidx.media3.test.utils.FakeExtractorOutput;
|
import androidx.media3.test.utils.FakeExtractorOutput;
|
||||||
import androidx.media3.test.utils.TestUtil;
|
import androidx.media3.test.utils.TestUtil;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@ -64,7 +57,6 @@ public class Mp4MuxerEndToEndAndroidTest {
|
|||||||
@Parameter public @MonotonicNonNull String inputFile;
|
@Parameter public @MonotonicNonNull String inputFile;
|
||||||
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
private static final String MP4_FILE_ASSET_DIRECTORY = "media/mp4/";
|
|
||||||
private final Context context = ApplicationProvider.getApplicationContext();
|
private final Context context = ApplicationProvider.getApplicationContext();
|
||||||
private @MonotonicNonNull String outputPath;
|
private @MonotonicNonNull String outputPath;
|
||||||
private @MonotonicNonNull FileOutputStream outputStream;
|
private @MonotonicNonNull FileOutputStream outputStream;
|
||||||
@ -90,7 +82,7 @@ public class Mp4MuxerEndToEndAndroidTest {
|
|||||||
new Mp4TimestampData(
|
new Mp4TimestampData(
|
||||||
/* creationTimestampSeconds= */ 100_000_000L,
|
/* creationTimestampSeconds= */ 100_000_000L,
|
||||||
/* modificationTimestampSeconds= */ 500_000_000L));
|
/* modificationTimestampSeconds= */ 500_000_000L));
|
||||||
feedInputDataToMuxer(mp4Muxer, checkNotNull(inputFile));
|
feedInputDataToMuxer(context, mp4Muxer, checkNotNull(inputFile));
|
||||||
} finally {
|
} finally {
|
||||||
if (mp4Muxer != null) {
|
if (mp4Muxer != null) {
|
||||||
mp4Muxer.close();
|
mp4Muxer.close();
|
||||||
@ -114,7 +106,7 @@ public class Mp4MuxerEndToEndAndroidTest {
|
|||||||
new Mp4TimestampData(
|
new Mp4TimestampData(
|
||||||
/* creationTimestampSeconds= */ 100_000_000L,
|
/* creationTimestampSeconds= */ 100_000_000L,
|
||||||
/* modificationTimestampSeconds= */ 500_000_000L));
|
/* modificationTimestampSeconds= */ 500_000_000L));
|
||||||
feedInputDataToMuxer(mp4Muxer, inputFile);
|
feedInputDataToMuxer(context, mp4Muxer, inputFile);
|
||||||
|
|
||||||
// Muxer not closed.
|
// Muxer not closed.
|
||||||
|
|
||||||
@ -128,100 +120,4 @@ public class Mp4MuxerEndToEndAndroidTest {
|
|||||||
fakeExtractorOutput,
|
fakeExtractorOutput,
|
||||||
AndroidMuxerTestUtil.getExpectedDumpFilePath("partial_" + inputFile));
|
AndroidMuxerTestUtil.getExpectedDumpFilePath("partial_" + inputFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void createFragmentedMp4File_fromInputFileSampleData_matchesExpected() throws IOException {
|
|
||||||
// Test case doesn't need to be parameterized, so skip all but one input file to avoid creating
|
|
||||||
// many dump files.
|
|
||||||
assumeTrue(checkNotNull(inputFile).equals(H265_HDR10_MP4));
|
|
||||||
@Nullable Mp4Muxer mp4Muxer = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
mp4Muxer =
|
|
||||||
new Mp4Muxer.Builder(checkNotNull(outputStream)).setFragmentedMp4Enabled(true).build();
|
|
||||||
mp4Muxer.addMetadata(
|
|
||||||
new Mp4TimestampData(
|
|
||||||
/* creationTimestampSeconds= */ 100_000_000L,
|
|
||||||
/* modificationTimestampSeconds= */ 500_000_000L));
|
|
||||||
feedInputDataToMuxer(mp4Muxer, inputFile);
|
|
||||||
} finally {
|
|
||||||
if (mp4Muxer != null) {
|
|
||||||
mp4Muxer.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FakeExtractorOutput fakeExtractorOutput =
|
|
||||||
TestUtil.extractAllSamplesFromFilePath(
|
|
||||||
new FragmentedMp4Extractor(), checkNotNull(outputPath));
|
|
||||||
DumpFileAsserts.assertOutput(
|
|
||||||
context,
|
|
||||||
fakeExtractorOutput,
|
|
||||||
AndroidMuxerTestUtil.getExpectedDumpFilePath(inputFile + "_fragmented"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void createFragmentedMp4File_fromInputFileSampleData_matchesExpectedBoxStructure()
|
|
||||||
throws IOException {
|
|
||||||
// Test case doesn't need to be parameterized, so skip all but one input file to avoid creating
|
|
||||||
// many dump files.
|
|
||||||
assumeTrue(checkNotNull(inputFile).equals(H265_HDR10_MP4));
|
|
||||||
@Nullable Mp4Muxer mp4Muxer = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
mp4Muxer =
|
|
||||||
new Mp4Muxer.Builder(checkNotNull(outputStream)).setFragmentedMp4Enabled(true).build();
|
|
||||||
mp4Muxer.addMetadata(
|
|
||||||
new Mp4TimestampData(
|
|
||||||
/* creationTimestampSeconds= */ 100_000_000L,
|
|
||||||
/* modificationTimestampSeconds= */ 500_000_000L));
|
|
||||||
feedInputDataToMuxer(mp4Muxer, inputFile);
|
|
||||||
} finally {
|
|
||||||
if (mp4Muxer != null) {
|
|
||||||
mp4Muxer.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DumpableMp4Box dumpableMp4Box =
|
|
||||||
new DumpableMp4Box(
|
|
||||||
ByteBuffer.wrap(TestUtil.getByteArrayFromFilePath(checkNotNull(outputPath))));
|
|
||||||
DumpFileAsserts.assertOutput(
|
|
||||||
context,
|
|
||||||
dumpableMp4Box,
|
|
||||||
AndroidMuxerTestUtil.getExpectedDumpFilePath(inputFile + "_fragmented_box_structure"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void feedInputDataToMuxer(Mp4Muxer mp4Muxer, String inputFileName) throws IOException {
|
|
||||||
MediaExtractor extractor = new MediaExtractor();
|
|
||||||
extractor.setDataSource(
|
|
||||||
context.getResources().getAssets().openFd(MP4_FILE_ASSET_DIRECTORY + inputFileName));
|
|
||||||
|
|
||||||
List<Mp4Muxer.TrackToken> addedTracks = new ArrayList<>();
|
|
||||||
int sortKey = 0;
|
|
||||||
for (int i = 0; i < extractor.getTrackCount(); i++) {
|
|
||||||
Mp4Muxer.TrackToken trackToken =
|
|
||||||
mp4Muxer.addTrack(
|
|
||||||
sortKey++, MediaFormatUtil.createFormatFromMediaFormat(extractor.getTrackFormat(i)));
|
|
||||||
addedTracks.add(trackToken);
|
|
||||||
extractor.selectTrack(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
|
||||||
bufferInfo.flags = extractor.getSampleFlags();
|
|
||||||
bufferInfo.offset = 0;
|
|
||||||
bufferInfo.presentationTimeUs = extractor.getSampleTime();
|
|
||||||
int sampleSize = (int) extractor.getSampleSize();
|
|
||||||
bufferInfo.size = sampleSize;
|
|
||||||
|
|
||||||
ByteBuffer sampleBuffer = ByteBuffer.allocateDirect(sampleSize);
|
|
||||||
extractor.readSampleData(sampleBuffer, /* offset= */ 0);
|
|
||||||
|
|
||||||
sampleBuffer.rewind();
|
|
||||||
|
|
||||||
mp4Muxer.writeSampleData(
|
|
||||||
addedTracks.get(extractor.getSampleTrackIndex()), sampleBuffer, bufferInfo);
|
|
||||||
} while (extractor.advance());
|
|
||||||
|
|
||||||
extractor.release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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 android.media.MediaCodec;
|
||||||
|
import androidx.media3.common.Format;
|
||||||
|
import androidx.media3.common.Metadata;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.container.MdtaMetadataEntry;
|
||||||
|
import androidx.media3.container.Mp4LocationData;
|
||||||
|
import androidx.media3.container.Mp4OrientationData;
|
||||||
|
import androidx.media3.container.Mp4TimestampData;
|
||||||
|
import androidx.media3.container.XmpData;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A muxer for creating a fragmented MP4 file.
|
||||||
|
*
|
||||||
|
* <p>The muxer supports writing H264, H265 and AV1 video, AAC audio and metadata.
|
||||||
|
*
|
||||||
|
* <p>All the operations are performed on the caller thread.
|
||||||
|
*
|
||||||
|
* <p>To create a fragmented MP4 file, the caller must:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Add tracks using {@link #addTrack(Format)} which will return a {@link Mp4Muxer.TrackToken}.
|
||||||
|
* <li>Use the associated {@link Mp4Muxer.TrackToken} when {@linkplain
|
||||||
|
* #writeSampleData(Mp4Muxer.TrackToken, ByteBuffer, MediaCodec.BufferInfo) writing samples}
|
||||||
|
* for that track.
|
||||||
|
* <li>{@link #close} the muxer when all data has been written.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Some key points:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>All tracks must be added before writing any samples.
|
||||||
|
* <li>The caller is responsible for ensuring that samples of different track types are well
|
||||||
|
* interleaved by calling {@link #writeSampleData(Mp4Muxer.TrackToken, ByteBuffer,
|
||||||
|
* MediaCodec.BufferInfo)} in an order that interleaves samples from different tracks.
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public final class FragmentedMp4Muxer implements Muxer {
|
||||||
|
private static final int DEFAULT_FRAGMENT_DURATION_US = 2_000_000;
|
||||||
|
|
||||||
|
private final FragmentedMp4Writer fragmentedMp4Writer;
|
||||||
|
private final MetadataCollector metadataCollector;
|
||||||
|
|
||||||
|
/** Creates an instance with default fragment duration. */
|
||||||
|
public FragmentedMp4Muxer(FileOutputStream fileOutputStream) {
|
||||||
|
this(fileOutputStream, DEFAULT_FRAGMENT_DURATION_US);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param fileOutputStream The {@link FileOutputStream} to write the media data to.
|
||||||
|
* @param fragmentDurationUs The fragment duration (in microseconds). The muxer will attempt to
|
||||||
|
* create fragments of the given duration but the actual duration might be greater depending
|
||||||
|
* upon the frequency of sync samples.
|
||||||
|
*/
|
||||||
|
public FragmentedMp4Muxer(FileOutputStream fileOutputStream, int fragmentDurationUs) {
|
||||||
|
checkNotNull(fileOutputStream);
|
||||||
|
metadataCollector = new MetadataCollector();
|
||||||
|
Mp4MoovStructure moovStructure =
|
||||||
|
new Mp4MoovStructure(
|
||||||
|
metadataCollector, Mp4Muxer.LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION);
|
||||||
|
fragmentedMp4Writer =
|
||||||
|
new FragmentedMp4Writer(
|
||||||
|
fileOutputStream, moovStructure, AnnexBToAvccConverter.DEFAULT, fragmentDurationUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TrackToken addTrack(Format format) {
|
||||||
|
return fragmentedMp4Writer.addTrack(/* sortKey= */ 1, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The samples are cached and are written in batches so the caller must not change the {@link
|
||||||
|
* ByteBuffer} and the {@link MediaCodec.BufferInfo} after calling this method.
|
||||||
|
*
|
||||||
|
* <p>Note: Out of order B-frames are currently not supported.
|
||||||
|
*
|
||||||
|
* @param trackToken The {@link TrackToken} for which this sample is being written.
|
||||||
|
* @param byteBuffer The encoded sample.
|
||||||
|
* @param bufferInfo The {@link MediaCodec.BufferInfo} related to this sample.
|
||||||
|
* @throws IOException If there is any error while writing data to the disk.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void writeSampleData(
|
||||||
|
TrackToken trackToken, ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo)
|
||||||
|
throws IOException {
|
||||||
|
fragmentedMp4Writer.writeSampleData(trackToken, byteBuffer, bufferInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>List of supported {@linkplain Metadata.Entry metadata entries}:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link Mp4OrientationData}
|
||||||
|
* <li>{@link Mp4LocationData}
|
||||||
|
* <li>{@link Mp4TimestampData}
|
||||||
|
* <li>{@link MdtaMetadataEntry}: Only {@linkplain MdtaMetadataEntry#TYPE_INDICATOR_STRING
|
||||||
|
* string type} or {@linkplain MdtaMetadataEntry#TYPE_INDICATOR_FLOAT32 float type} value is
|
||||||
|
* supported.
|
||||||
|
* <li>{@link XmpData}
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param metadata The {@linkplain Metadata.Entry metadata}. An {@link IllegalArgumentException}
|
||||||
|
* is thrown if the {@linkplain Metadata.Entry metadata} is not supported.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void addMetadata(Metadata.Entry metadata) {
|
||||||
|
checkArgument(Mp4Utils.isMetadataSupported(metadata), "Unsupported metadata");
|
||||||
|
metadataCollector.addMetadata(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
fragmentedMp4Writer.close();
|
||||||
|
}
|
||||||
|
}
|
@ -17,7 +17,6 @@ package androidx.media3.muxer;
|
|||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
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.muxer.Mp4Utils.UNSIGNED_INT_MAX_VALUE;
|
|
||||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||||
|
|
||||||
import android.media.MediaCodec.BufferInfo;
|
import android.media.MediaCodec.BufferInfo;
|
||||||
@ -25,14 +24,12 @@ import androidx.annotation.IntDef;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.Metadata;
|
import androidx.media3.common.Metadata;
|
||||||
import androidx.media3.common.MimeTypes;
|
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.container.MdtaMetadataEntry;
|
import androidx.media3.container.MdtaMetadataEntry;
|
||||||
import androidx.media3.container.Mp4LocationData;
|
import androidx.media3.container.Mp4LocationData;
|
||||||
import androidx.media3.container.Mp4OrientationData;
|
import androidx.media3.container.Mp4OrientationData;
|
||||||
import androidx.media3.container.Mp4TimestampData;
|
import androidx.media3.container.Mp4TimestampData;
|
||||||
import androidx.media3.container.XmpData;
|
import androidx.media3.container.XmpData;
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -96,8 +93,6 @@ public final class Mp4Muxer implements Muxer {
|
|||||||
private final FileOutputStream fileOutputStream;
|
private final FileOutputStream fileOutputStream;
|
||||||
|
|
||||||
private @LastFrameDurationBehavior int lastFrameDurationBehavior;
|
private @LastFrameDurationBehavior int lastFrameDurationBehavior;
|
||||||
private boolean fragmentedMp4Enabled;
|
|
||||||
private int fragmentDurationUs;
|
|
||||||
@Nullable private AnnexBToAvccConverter annexBToAvccConverter;
|
@Nullable private AnnexBToAvccConverter annexBToAvccConverter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,7 +103,6 @@ public final class Mp4Muxer implements Muxer {
|
|||||||
public Builder(FileOutputStream fileOutputStream) {
|
public Builder(FileOutputStream fileOutputStream) {
|
||||||
this.fileOutputStream = checkNotNull(fileOutputStream);
|
this.fileOutputStream = checkNotNull(fileOutputStream);
|
||||||
lastFrameDurationBehavior = LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME;
|
lastFrameDurationBehavior = LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME;
|
||||||
fragmentDurationUs = DEFAULT_FRAGMENT_DURATION_US;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -136,37 +130,6 @@ public final class Mp4Muxer implements Muxer {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether to enable writing a fragmented MP4.
|
|
||||||
*
|
|
||||||
* <p>The default value is {@code false}.
|
|
||||||
*/
|
|
||||||
@CanIgnoreReturnValue
|
|
||||||
public Mp4Muxer.Builder setFragmentedMp4Enabled(boolean enabled) {
|
|
||||||
fragmentedMp4Enabled = enabled;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets fragment duration for the {@linkplain #setFragmentedMp4Enabled(boolean) fragmented MP4}.
|
|
||||||
*
|
|
||||||
* <p>Muxer will attempt to create fragments of the given duration but the actual duration might
|
|
||||||
* be greater depending upon the frequency of sync samples.
|
|
||||||
*
|
|
||||||
* <p>The duration is ignored for {@linkplain #setFragmentedMp4Enabled(boolean) non fragmented
|
|
||||||
* MP4}.
|
|
||||||
*
|
|
||||||
* <p>The default value is {@link #DEFAULT_FRAGMENT_DURATION_US}.
|
|
||||||
*
|
|
||||||
* @param fragmentDurationUs The fragment duration in microseconds.
|
|
||||||
* @return The {@link Mp4Muxer.Builder}.
|
|
||||||
*/
|
|
||||||
@CanIgnoreReturnValue
|
|
||||||
public Mp4Muxer.Builder setFragmentDurationUs(int fragmentDurationUs) {
|
|
||||||
this.fragmentDurationUs = fragmentDurationUs;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Builds an {@link Mp4Muxer} instance. */
|
/** Builds an {@link Mp4Muxer} instance. */
|
||||||
public Mp4Muxer build() {
|
public Mp4Muxer build() {
|
||||||
MetadataCollector metadataCollector = new MetadataCollector();
|
MetadataCollector metadataCollector = new MetadataCollector();
|
||||||
@ -174,53 +137,20 @@ public final class Mp4Muxer implements Muxer {
|
|||||||
new Mp4MoovStructure(metadataCollector, lastFrameDurationBehavior);
|
new Mp4MoovStructure(metadataCollector, lastFrameDurationBehavior);
|
||||||
AnnexBToAvccConverter avccConverter =
|
AnnexBToAvccConverter avccConverter =
|
||||||
annexBToAvccConverter == null ? AnnexBToAvccConverter.DEFAULT : annexBToAvccConverter;
|
annexBToAvccConverter == null ? AnnexBToAvccConverter.DEFAULT : annexBToAvccConverter;
|
||||||
Mp4Writer mp4Writer =
|
BasicMp4Writer mp4Writer = new BasicMp4Writer(fileOutputStream, moovStructure, avccConverter);
|
||||||
fragmentedMp4Enabled
|
|
||||||
? new FragmentedMp4Writer(
|
|
||||||
fileOutputStream, moovStructure, avccConverter, fragmentDurationUs)
|
|
||||||
: new BasicMp4Writer(fileOutputStream, moovStructure, avccConverter);
|
|
||||||
|
|
||||||
return new Mp4Muxer(mp4Writer, metadataCollector);
|
return new Mp4Muxer(mp4Writer, metadataCollector);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A list of supported video sample mime types. */
|
private final BasicMp4Writer mp4Writer;
|
||||||
public static final ImmutableList<String> SUPPORTED_VIDEO_SAMPLE_MIME_TYPES =
|
|
||||||
ImmutableList.of(MimeTypes.VIDEO_H264, MimeTypes.VIDEO_H265, MimeTypes.VIDEO_AV1);
|
|
||||||
|
|
||||||
/** A list of supported audio sample mime types. */
|
|
||||||
public static final ImmutableList<String> SUPPORTED_AUDIO_SAMPLE_MIME_TYPES =
|
|
||||||
ImmutableList.of(MimeTypes.AUDIO_AAC);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default fragment duration for the {@linkplain Builder#setFragmentedMp4Enabled(boolean)
|
|
||||||
* fragmented MP4}.
|
|
||||||
*/
|
|
||||||
public static final int DEFAULT_FRAGMENT_DURATION_US = 2_000_000;
|
|
||||||
|
|
||||||
private final Mp4Writer mp4Writer;
|
|
||||||
private final MetadataCollector metadataCollector;
|
private final MetadataCollector metadataCollector;
|
||||||
|
|
||||||
private Mp4Muxer(Mp4Writer mp4Writer, MetadataCollector metadataCollector) {
|
private Mp4Muxer(BasicMp4Writer mp4Writer, MetadataCollector metadataCollector) {
|
||||||
this.mp4Writer = mp4Writer;
|
this.mp4Writer = mp4Writer;
|
||||||
this.metadataCollector = metadataCollector;
|
this.metadataCollector = metadataCollector;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether a given {@link Metadata.Entry metadata} is supported.
|
|
||||||
*
|
|
||||||
* <p>For the list of supported metadata refer to {@link Mp4Muxer#addMetadata(Metadata.Entry)}.
|
|
||||||
*/
|
|
||||||
public static boolean isMetadataSupported(Metadata.Entry metadata) {
|
|
||||||
return metadata instanceof Mp4OrientationData
|
|
||||||
|| metadata instanceof Mp4LocationData
|
|
||||||
|| (metadata instanceof Mp4TimestampData
|
|
||||||
&& isMp4TimestampDataSupported((Mp4TimestampData) metadata))
|
|
||||||
|| (metadata instanceof MdtaMetadataEntry
|
|
||||||
&& isMdtaMetadataEntrySupported((MdtaMetadataEntry) metadata))
|
|
||||||
|| metadata instanceof XmpData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*
|
*
|
||||||
@ -258,8 +188,8 @@ public final class Mp4Muxer implements Muxer {
|
|||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*
|
*
|
||||||
* <p>The samples are cached and are written in batches so the caller must not change/release the
|
* <p>The samples are cached and are written in batches so the caller must not change the {@link
|
||||||
* {@link ByteBuffer} and the {@link BufferInfo} after calling this method.
|
* ByteBuffer} and the {@link BufferInfo} after calling this method.
|
||||||
*
|
*
|
||||||
* <p>Note: Out of order B-frames are currently not supported.
|
* <p>Note: Out of order B-frames are currently not supported.
|
||||||
*
|
*
|
||||||
@ -290,11 +220,11 @@ public final class Mp4Muxer implements Muxer {
|
|||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @param metadata The {@linkplain Metadata.Entry metadata}. An {@link IllegalArgumentException}
|
* @param metadata The {@linkplain Metadata.Entry metadata}. An {@link IllegalArgumentException}
|
||||||
* is throw if the {@linkplain Metadata.Entry metadata} is not supported.
|
* is thrown if the {@linkplain Metadata.Entry metadata} is not supported.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void addMetadata(Metadata.Entry metadata) {
|
public void addMetadata(Metadata.Entry metadata) {
|
||||||
checkArgument(isMetadataSupported(metadata), "Unsupported metadata");
|
checkArgument(Mp4Utils.isMetadataSupported(metadata), "Unsupported metadata");
|
||||||
metadataCollector.addMetadata(metadata);
|
metadataCollector.addMetadata(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,14 +232,4 @@ public final class Mp4Muxer implements Muxer {
|
|||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
mp4Writer.close();
|
mp4Writer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isMdtaMetadataEntrySupported(MdtaMetadataEntry mdtaMetadataEntry) {
|
|
||||||
return mdtaMetadataEntry.typeIndicator == MdtaMetadataEntry.TYPE_INDICATOR_STRING
|
|
||||||
|| mdtaMetadataEntry.typeIndicator == MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isMp4TimestampDataSupported(Mp4TimestampData timestampData) {
|
|
||||||
return timestampData.creationTimestampSeconds <= UNSIGNED_INT_MAX_VALUE
|
|
||||||
&& timestampData.modificationTimestampSeconds <= UNSIGNED_INT_MAX_VALUE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.muxer;
|
package androidx.media3.muxer;
|
||||||
|
|
||||||
|
import androidx.media3.common.Metadata;
|
||||||
|
import androidx.media3.container.MdtaMetadataEntry;
|
||||||
|
import androidx.media3.container.Mp4LocationData;
|
||||||
|
import androidx.media3.container.Mp4OrientationData;
|
||||||
|
import androidx.media3.container.Mp4TimestampData;
|
||||||
|
import androidx.media3.container.XmpData;
|
||||||
|
|
||||||
/** Utilities for MP4 files. */
|
/** Utilities for MP4 files. */
|
||||||
/* package */ final class Mp4Utils {
|
/* package */ final class Mp4Utils {
|
||||||
/* Total number of bytes in an integer. */
|
/* Total number of bytes in an integer. */
|
||||||
@ -48,4 +55,25 @@ package androidx.media3.muxer;
|
|||||||
public static long usFromVu(long timestampVu, long videoUnitTimebase) {
|
public static long usFromVu(long timestampVu, long videoUnitTimebase) {
|
||||||
return timestampVu * 1_000_000L / videoUnitTimebase;
|
return timestampVu * 1_000_000L / videoUnitTimebase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns whether a given {@link Metadata.Entry metadata} is supported. */
|
||||||
|
public static boolean isMetadataSupported(Metadata.Entry metadata) {
|
||||||
|
return metadata instanceof Mp4OrientationData
|
||||||
|
|| metadata instanceof Mp4LocationData
|
||||||
|
|| (metadata instanceof Mp4TimestampData
|
||||||
|
&& isMp4TimestampDataSupported((Mp4TimestampData) metadata))
|
||||||
|
|| (metadata instanceof MdtaMetadataEntry
|
||||||
|
&& isMdtaMetadataEntrySupported((MdtaMetadataEntry) metadata))
|
||||||
|
|| metadata instanceof XmpData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isMdtaMetadataEntrySupported(MdtaMetadataEntry mdtaMetadataEntry) {
|
||||||
|
return mdtaMetadataEntry.typeIndicator == MdtaMetadataEntry.TYPE_INDICATOR_STRING
|
||||||
|
|| mdtaMetadataEntry.typeIndicator == MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isMp4TimestampDataSupported(Mp4TimestampData timestampData) {
|
||||||
|
return timestampData.creationTimestampSeconds <= UNSIGNED_INT_MAX_VALUE
|
||||||
|
&& timestampData.modificationTimestampSeconds <= UNSIGNED_INT_MAX_VALUE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,16 +15,18 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.transformer;
|
package androidx.media3.transformer;
|
||||||
|
|
||||||
import static androidx.media3.muxer.Mp4Muxer.SUPPORTED_AUDIO_SAMPLE_MIME_TYPES;
|
|
||||||
import static androidx.media3.muxer.Mp4Muxer.SUPPORTED_VIDEO_SAMPLE_MIME_TYPES;
|
|
||||||
|
|
||||||
import android.media.MediaCodec.BufferInfo;
|
import android.media.MediaCodec.BufferInfo;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.Metadata;
|
import androidx.media3.common.Metadata;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.container.MdtaMetadataEntry;
|
||||||
|
import androidx.media3.container.Mp4LocationData;
|
||||||
import androidx.media3.container.Mp4OrientationData;
|
import androidx.media3.container.Mp4OrientationData;
|
||||||
|
import androidx.media3.container.Mp4TimestampData;
|
||||||
|
import androidx.media3.container.XmpData;
|
||||||
|
import androidx.media3.muxer.FragmentedMp4Muxer;
|
||||||
import androidx.media3.muxer.Mp4Muxer;
|
import androidx.media3.muxer.Mp4Muxer;
|
||||||
import androidx.media3.muxer.Muxer.TrackToken;
|
import androidx.media3.muxer.Muxer.TrackToken;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
@ -39,7 +41,7 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
/** {@link Muxer} implementation that uses a {@link Mp4Muxer}. */
|
/** {@link Muxer} implementation that uses an {@link Mp4Muxer} or {@link FragmentedMp4Muxer}. */
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class InAppMuxer implements Muxer {
|
public final class InAppMuxer implements Muxer {
|
||||||
|
|
||||||
@ -68,7 +70,7 @@ public final class InAppMuxer implements Muxer {
|
|||||||
|
|
||||||
/** Creates a {@link Builder} instance with default values. */
|
/** Creates a {@link Builder} instance with default values. */
|
||||||
public Builder() {
|
public Builder() {
|
||||||
fragmentDurationUs = Mp4Muxer.DEFAULT_FRAGMENT_DURATION_US;
|
fragmentDurationUs = C.LENGTH_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -85,14 +87,17 @@ public final class InAppMuxer implements Muxer {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** See {@link Mp4Muxer.Builder#setFragmentedMp4Enabled(boolean)}. */
|
/** Sets whether to produce a fragmented MP4. */
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public Builder setFragmentedMp4Enabled(boolean fragmentedMp4Enabled) {
|
public Builder setFragmentedMp4Enabled(boolean fragmentedMp4Enabled) {
|
||||||
this.fragmentedMp4Enabled = fragmentedMp4Enabled;
|
this.fragmentedMp4Enabled = fragmentedMp4Enabled;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** See {@link Mp4Muxer.Builder#setFragmentDurationUs(int)}. */
|
/**
|
||||||
|
* Sets the fragment duration if the output file is {@link #setFragmentedMp4Enabled(boolean)
|
||||||
|
* fragmented}.
|
||||||
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public Builder setFragmentDurationUs(int fragmentDurationUs) {
|
public Builder setFragmentDurationUs(int fragmentDurationUs) {
|
||||||
this.fragmentDurationUs = fragmentDurationUs;
|
this.fragmentDurationUs = fragmentDurationUs;
|
||||||
@ -105,6 +110,14 @@ public final class InAppMuxer implements Muxer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A list of supported video sample MIME types. */
|
||||||
|
private static final ImmutableList<String> SUPPORTED_VIDEO_SAMPLE_MIME_TYPES =
|
||||||
|
ImmutableList.of(MimeTypes.VIDEO_H264, MimeTypes.VIDEO_H265, MimeTypes.VIDEO_AV1);
|
||||||
|
|
||||||
|
/** A list of supported audio sample MIME types. */
|
||||||
|
private static final ImmutableList<String> SUPPORTED_AUDIO_SAMPLE_MIME_TYPES =
|
||||||
|
ImmutableList.of(MimeTypes.AUDIO_AAC);
|
||||||
|
|
||||||
private final @Nullable MetadataProvider metadataProvider;
|
private final @Nullable MetadataProvider metadataProvider;
|
||||||
private final boolean fragmentedMp4Enabled;
|
private final boolean fragmentedMp4Enabled;
|
||||||
private final int fragmentDurationUs;
|
private final int fragmentDurationUs;
|
||||||
@ -127,12 +140,13 @@ public final class InAppMuxer implements Muxer {
|
|||||||
throw new MuxerException("Error creating file output stream", e);
|
throw new MuxerException("Error creating file output stream", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
Mp4Muxer mp4Muxer =
|
androidx.media3.muxer.Muxer muxer =
|
||||||
new Mp4Muxer.Builder(outputStream)
|
fragmentedMp4Enabled
|
||||||
.setFragmentedMp4Enabled(fragmentedMp4Enabled)
|
? fragmentDurationUs != C.LENGTH_UNSET
|
||||||
.setFragmentDurationUs(fragmentDurationUs)
|
? new FragmentedMp4Muxer(outputStream, fragmentDurationUs)
|
||||||
.build();
|
: new FragmentedMp4Muxer(outputStream)
|
||||||
return new InAppMuxer(mp4Muxer, metadataProvider);
|
: new Mp4Muxer.Builder(outputStream).build();
|
||||||
|
return new InAppMuxer(muxer, metadataProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -146,14 +160,15 @@ public final class InAppMuxer implements Muxer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Mp4Muxer mp4Muxer;
|
private final androidx.media3.muxer.Muxer muxer;
|
||||||
private final @Nullable MetadataProvider metadataProvider;
|
private final @Nullable MetadataProvider metadataProvider;
|
||||||
private final List<TrackToken> trackTokenList;
|
private final List<TrackToken> trackTokenList;
|
||||||
private final BufferInfo bufferInfo;
|
private final BufferInfo bufferInfo;
|
||||||
private final Set<Metadata.Entry> metadataEntries;
|
private final Set<Metadata.Entry> metadataEntries;
|
||||||
|
|
||||||
private InAppMuxer(Mp4Muxer mp4Muxer, @Nullable MetadataProvider metadataProvider) {
|
private InAppMuxer(
|
||||||
this.mp4Muxer = mp4Muxer;
|
androidx.media3.muxer.Muxer muxer, @Nullable MetadataProvider metadataProvider) {
|
||||||
|
this.muxer = muxer;
|
||||||
this.metadataProvider = metadataProvider;
|
this.metadataProvider = metadataProvider;
|
||||||
trackTokenList = new ArrayList<>();
|
trackTokenList = new ArrayList<>();
|
||||||
bufferInfo = new BufferInfo();
|
bufferInfo = new BufferInfo();
|
||||||
@ -162,12 +177,11 @@ public final class InAppMuxer implements Muxer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int addTrack(Format format) {
|
public int addTrack(Format format) {
|
||||||
// Keep same sort key as no specific sort order is required.
|
TrackToken trackToken = muxer.addTrack(format);
|
||||||
TrackToken trackToken = mp4Muxer.addTrack(/* sortKey= */ 0, format);
|
|
||||||
trackTokenList.add(trackToken);
|
trackTokenList.add(trackToken);
|
||||||
|
|
||||||
if (MimeTypes.isVideo(format.sampleMimeType)) {
|
if (MimeTypes.isVideo(format.sampleMimeType)) {
|
||||||
mp4Muxer.addMetadata(new Mp4OrientationData(format.rotationDegrees));
|
muxer.addMetadata(new Mp4OrientationData(format.rotationDegrees));
|
||||||
}
|
}
|
||||||
|
|
||||||
return trackTokenList.size() - 1;
|
return trackTokenList.size() - 1;
|
||||||
@ -195,7 +209,7 @@ public final class InAppMuxer implements Muxer {
|
|||||||
bufferInfo.presentationTimeUs,
|
bufferInfo.presentationTimeUs,
|
||||||
bufferInfo.flags);
|
bufferInfo.flags);
|
||||||
|
|
||||||
mp4Muxer.writeSampleData(trackTokenList.get(trackIndex), byteBufferCopy, bufferInfoCopy);
|
muxer.writeSampleData(trackTokenList.get(trackIndex), byteBufferCopy, bufferInfoCopy);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new MuxerException(
|
throw new MuxerException(
|
||||||
"Failed to write sample for trackIndex="
|
"Failed to write sample for trackIndex="
|
||||||
@ -212,7 +226,7 @@ public final class InAppMuxer implements Muxer {
|
|||||||
public void addMetadata(Metadata metadata) {
|
public void addMetadata(Metadata metadata) {
|
||||||
for (int i = 0; i < metadata.length(); i++) {
|
for (int i = 0; i < metadata.length(); i++) {
|
||||||
Metadata.Entry entry = metadata.get(i);
|
Metadata.Entry entry = metadata.get(i);
|
||||||
if (Mp4Muxer.isMetadataSupported(entry)) {
|
if (isMetadataSupported(entry)) {
|
||||||
metadataEntries.add(entry);
|
metadataEntries.add(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -223,7 +237,7 @@ public final class InAppMuxer implements Muxer {
|
|||||||
writeMetadata();
|
writeMetadata();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mp4Muxer.close();
|
muxer.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new MuxerException("Error closing muxer", e);
|
throw new MuxerException("Error closing muxer", e);
|
||||||
}
|
}
|
||||||
@ -238,7 +252,22 @@ public final class InAppMuxer implements Muxer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (Metadata.Entry entry : metadataEntries) {
|
for (Metadata.Entry entry : metadataEntries) {
|
||||||
mp4Muxer.addMetadata(entry);
|
muxer.addMetadata(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns whether a given {@link Metadata.Entry metadata} is supported. */
|
||||||
|
private static boolean isMetadataSupported(Metadata.Entry metadata) {
|
||||||
|
return metadata instanceof Mp4OrientationData
|
||||||
|
|| metadata instanceof Mp4LocationData
|
||||||
|
|| metadata instanceof Mp4TimestampData
|
||||||
|
|| (metadata instanceof MdtaMetadataEntry
|
||||||
|
&& isMdtaMetadataEntrySupported((MdtaMetadataEntry) metadata))
|
||||||
|
|| metadata instanceof XmpData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isMdtaMetadataEntrySupported(MdtaMetadataEntry mdtaMetadataEntry) {
|
||||||
|
return mdtaMetadataEntry.typeIndicator == MdtaMetadataEntry.TYPE_INDICATOR_STRING
|
||||||
|
|| mdtaMetadataEntry.typeIndicator == MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user