Split InAppMuxer into InApp Mp4Muxer and FragmentedMp4Muxer

This is pre work required to remove `Muxer.java` interface
from the muxer module.
`Mp4Muxer` and `FragmentedMp4Muxer` will no longer implement
the `Muxer` interface.

PiperOrigin-RevId: 716669531
This commit is contained in:
sheenachhabra 2025-01-17 07:58:06 -08:00 committed by Copybara-Service
parent a4d9a3e096
commit 4ac4f7e2e0
17 changed files with 328 additions and 208 deletions

View File

@ -29,6 +29,10 @@
by the user of the device. Apps can opt-out of contributing to platform
diagnostics for Transformer with
`Transformer.Builder.setUsePlatformDiagnostics(false)`.
* Split `InAppMuxer` into `InAppMp4Muxer` and `InAppFragmentedMp4Muxer`.
`InAppMp4Muxer` is to be used for producing a non-fragmented MP4 file,
while `InAppFragmentedMp4Muxer` is to be used for producing a fragmented
MP4 file.
* Track Selection:
* Extractors:
* Fix handling of NAL units with lengths expressed in 1 or 2 bytes (rather

View File

@ -57,7 +57,8 @@ import androidx.media3.transformer.EditedMediaItemSequence;
import androidx.media3.transformer.Effects;
import androidx.media3.transformer.ExportException;
import androidx.media3.transformer.ExportResult;
import androidx.media3.transformer.InAppMuxer;
import androidx.media3.transformer.InAppFragmentedMp4Muxer;
import androidx.media3.transformer.InAppMp4Muxer;
import androidx.media3.transformer.JsonUtil;
import androidx.media3.transformer.Transformer;
import androidx.media3.ui.PlayerView;
@ -362,21 +363,20 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
enableDebugTracingCheckBox.setOnCheckedChangeListener(
(buttonView, isChecked) -> DebugTraceUtil.enableTracing = isChecked);
// Connect producing fragmented MP4 to using Media3 Muxer
CheckBox useMedia3MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_muxer_checkbox);
CheckBox produceFragmentedMp4CheckBox =
exportSettingsDialogView.findViewById(R.id.produce_fragmented_mp4_checkbox);
useMedia3MuxerCheckBox.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (!isChecked) {
produceFragmentedMp4CheckBox.setChecked(false);
}
});
produceFragmentedMp4CheckBox.setOnCheckedChangeListener(
CheckBox useMedia3Mp4MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_mp4_muxer_checkbox);
CheckBox useMedia3FragmentedMp4MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_fragmented_mp4_muxer_checkbox);
useMedia3Mp4MuxerCheckBox.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (isChecked) {
useMedia3MuxerCheckBox.setChecked(true);
useMedia3FragmentedMp4MuxerCheckBox.setChecked(false);
}
});
useMedia3FragmentedMp4MuxerCheckBox.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (isChecked) {
useMedia3Mp4MuxerCheckBox.setChecked(false);
}
});
@ -419,15 +419,15 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
transformerBuilder.setVideoMimeType(selectedVideoMimeType);
}
CheckBox useMedia3MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_muxer_checkbox);
CheckBox produceFragmentedMp4CheckBox =
exportSettingsDialogView.findViewById(R.id.produce_fragmented_mp4_checkbox);
if (useMedia3MuxerCheckBox.isChecked()) {
transformerBuilder.setMuxerFactory(
new InAppMuxer.Factory.Builder()
.setOutputFragmentedMp4(produceFragmentedMp4CheckBox.isChecked())
.build());
CheckBox useMedia3Mp4MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_mp4_muxer_checkbox);
CheckBox useMedia3FragmentedMp4MuxerCheckBox =
exportSettingsDialogView.findViewById(R.id.use_media3_fragmented_mp4_muxer_checkbox);
if (useMedia3Mp4MuxerCheckBox.isChecked()) {
transformerBuilder.setMuxerFactory(new InAppMp4Muxer.Factory());
}
if (useMedia3FragmentedMp4MuxerCheckBox.isChecked()) {
transformerBuilder.setMuxerFactory(new InAppFragmentedMp4Muxer.Factory());
}
transformer =

View File

@ -79,12 +79,12 @@
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:text="@string/use_media3_muxer"
android:text="@string/use_media3_mp4_muxer"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1" />
<CheckBox
android:id="@+id/use_media3_muxer_checkbox"
android:id="@+id/use_media3_mp4_muxer_checkbox"
android:layout_gravity="end"
android:checked="false"
android:layout_height="wrap_content"
@ -96,12 +96,12 @@
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:text="@string/produce_fragmented_mp4"
android:text="@string/use_media3_fragmented_mp4_muxer"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1" />
<CheckBox
android:id="@+id/produce_fragmented_mp4_checkbox"
android:id="@+id/use_media3_fragmented_mp4_muxer_checkbox"
android:layout_gravity="end"
android:checked="false"
android:layout_height="wrap_content"

View File

@ -34,6 +34,6 @@
<string name="output_audio_mime_type" translatable="false">Output audio MIME type</string>
<string name="output_video_mime_type" translatable="false">Output video MIME type</string>
<string name="enable_debug_tracing" translatable="false">Enable debug tracing</string>
<string name="use_media3_muxer" translatable="false">Use Media3 muxer</string>
<string name="produce_fragmented_mp4" translatable="false">Produce fragmented MP4</string>
<string name="use_media3_mp4_muxer" translatable="false">Use Media3 Mp4Muxer</string>
<string name="use_media3_fragmented_mp4_muxer" translatable="false">Use Media3 FragmentedMp4Muxer</string>
</resources>

View File

@ -77,8 +77,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
public static final String ENABLE_ANALYZER_MODE = "enable_analyzer_mode";
public static final String ENABLE_DEBUG_PREVIEW = "enable_debug_preview";
public static final String ABORT_SLOW_EXPORT = "abort_slow_export";
public static final String USE_MEDIA3_MUXER = "use_media3_muxer";
public static final String PRODUCE_FRAGMENTED_MP4 = "produce_fragmented_mp4";
public static final String USE_MEDIA3_MP4_MUXER = "use_media3_mp4_muxer";
public static final String USE_MEDIA3_FRAGMENTED_MP4_MUXER = "use_media3_fragmented_mp4_muxer";
public static final String HDR_MODE = "hdr_mode";
public static final String AUDIO_EFFECTS_SELECTIONS = "audio_effects_selections";
public static final String VIDEO_EFFECTS_SELECTIONS = "video_effects_selections";
@ -177,8 +177,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
private CheckBox enableDebugPreviewCheckBox;
private CheckBox enableDebugTracingCheckBox;
private CheckBox abortSlowExportCheckBox;
private CheckBox useMedia3Muxer;
private CheckBox produceFragmentedMp4CheckBox;
private CheckBox useMedia3Mp4Muxer;
private CheckBox useMedia3FragmentedMp4Muxer;
private Spinner hdrModeSpinner;
private Button selectAudioEffectsButton;
private Button selectVideoEffectsButton;
@ -303,18 +303,18 @@ public final class ConfigurationActivity extends AppCompatActivity {
(buttonView, isChecked) -> DebugTraceUtil.enableTracing = isChecked);
abortSlowExportCheckBox = findViewById(R.id.abort_slow_export_checkbox);
useMedia3Muxer = findViewById(R.id.use_media3_muxer_checkbox);
produceFragmentedMp4CheckBox = findViewById(R.id.produce_fragmented_mp4_checkbox);
useMedia3Muxer.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (!isChecked) {
produceFragmentedMp4CheckBox.setChecked(false);
}
});
produceFragmentedMp4CheckBox.setOnCheckedChangeListener(
useMedia3Mp4Muxer = findViewById(R.id.use_media3_mp4_muxer_checkbox);
useMedia3FragmentedMp4Muxer = findViewById(R.id.use_media3_fragmented_mp4_muxer_checkbox);
useMedia3Mp4Muxer.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (isChecked) {
useMedia3Muxer.setChecked(true);
useMedia3FragmentedMp4Muxer.setChecked(false);
}
});
useMedia3FragmentedMp4Muxer.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (isChecked) {
useMedia3Mp4Muxer.setChecked(false);
}
});
@ -407,8 +407,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
bundle.putBoolean(ENABLE_ANALYZER_MODE, enableAnalyzerModeCheckBox.isChecked());
bundle.putBoolean(ENABLE_DEBUG_PREVIEW, enableDebugPreviewCheckBox.isChecked());
bundle.putBoolean(ABORT_SLOW_EXPORT, abortSlowExportCheckBox.isChecked());
bundle.putBoolean(USE_MEDIA3_MUXER, useMedia3Muxer.isChecked());
bundle.putBoolean(PRODUCE_FRAGMENTED_MP4, produceFragmentedMp4CheckBox.isChecked());
bundle.putBoolean(USE_MEDIA3_MP4_MUXER, useMedia3Mp4Muxer.isChecked());
bundle.putBoolean(USE_MEDIA3_FRAGMENTED_MP4_MUXER, useMedia3FragmentedMp4Muxer.isChecked());
String selectedHdrMode = String.valueOf(hdrModeSpinner.getSelectedItem());
bundle.putInt(HDR_MODE, HDR_MODE_DESCRIPTIONS.get(selectedHdrMode));
bundle.putBooleanArray(AUDIO_EFFECTS_SELECTIONS, audioEffectsSelections);

View File

@ -88,7 +88,8 @@ import androidx.media3.transformer.Effects;
import androidx.media3.transformer.ExperimentalAnalyzerModeFactory;
import androidx.media3.transformer.ExportException;
import androidx.media3.transformer.ExportResult;
import androidx.media3.transformer.InAppMuxer;
import androidx.media3.transformer.InAppFragmentedMp4Muxer;
import androidx.media3.transformer.InAppMp4Muxer;
import androidx.media3.transformer.JsonUtil;
import androidx.media3.transformer.ProgressHolder;
import androidx.media3.transformer.Transformer;
@ -302,12 +303,12 @@ public final class TransformerActivity extends AppCompatActivity {
transformerBuilder.setMaxDelayBetweenMuxerSamplesMs(C.TIME_UNSET);
}
if (bundle.getBoolean(ConfigurationActivity.USE_MEDIA3_MUXER)) {
transformerBuilder.setMuxerFactory(
new InAppMuxer.Factory.Builder()
.setOutputFragmentedMp4(
bundle.getBoolean(ConfigurationActivity.PRODUCE_FRAGMENTED_MP4))
.build());
if (bundle.getBoolean(ConfigurationActivity.USE_MEDIA3_MP4_MUXER)) {
transformerBuilder.setMuxerFactory(new InAppMp4Muxer.Factory());
}
if (bundle.getBoolean(ConfigurationActivity.USE_MEDIA3_FRAGMENTED_MP4_MUXER)) {
transformerBuilder.setMuxerFactory(new InAppFragmentedMp4Muxer.Factory());
}
if (bundle.getBoolean(ConfigurationActivity.ENABLE_DEBUG_PREVIEW)) {

View File

@ -239,18 +239,18 @@
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:text="@string/use_media3_muxer" />
android:text="@string/use_media3_mp4_muxer" />
<CheckBox
android:id="@+id/use_media3_muxer_checkbox"
android:id="@+id/use_media3_mp4_muxer_checkbox"
android:layout_gravity="end"/>
</TableRow>
<TableRow
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:text="@string/produce_fragmented_mp4" />
android:text="@string/use_media3_fragmented_mp4_muxer" />
<CheckBox
android:id="@+id/produce_fragmented_mp4_checkbox"
android:id="@+id/use_media3_fragmented_mp4_muxer_checkbox"
android:layout_gravity="end"/>
</TableRow>
<TableRow

View File

@ -32,8 +32,8 @@
<string name="enable_debug_preview" translatable="false">Enable debug preview</string>
<string name="enable_debug_tracing" translatable="false">Enable debug tracing</string>
<string name="abort_slow_export" translatable="false">Abort slow export</string>
<string name="use_media3_muxer" translatable="false">Use Media3 muxer</string>
<string name="produce_fragmented_mp4" translatable="false">Produce fragmented MP4</string>
<string name="use_media3_mp4_muxer" translatable="false">Use Media3 Mp4Muxer</string>
<string name="use_media3_fragmented_mp4_muxer" translatable="false">Use Media3 FragmentedMp4Muxer</string>
<string name="trim" translatable="false">Trim</string>
<string name="hdr_mode" translatable="false">HDR mode</string>
<string name="select_audio_effects" translatable="false">Add audio effects</string>

View File

@ -1385,7 +1385,7 @@ public final class AndroidTestUtil {
/** Returns a {@link Muxer.Factory} depending upon the API level. */
public static Muxer.Factory getMuxerFactoryBasedOnApi() {
// MediaMuxer supports B-frame from API > 24.
return SDK_INT > 24 ? new DefaultMuxer.Factory() : new InAppMuxer.Factory.Builder().build();
return SDK_INT > 24 ? new DefaultMuxer.Factory() : new InAppMp4Muxer.Factory();
}
private static boolean canDecode(Format format) throws MediaCodecUtil.DecoderQueryException {

View File

@ -1815,7 +1815,7 @@ public class TransformerEndToEndTest {
context,
new Transformer.Builder(context)
.setVideoMimeType(MimeTypes.VIDEO_H265)
.setMuxerFactory(new InAppMuxer.Factory.Builder().build())
.setMuxerFactory(new InAppMp4Muxer.Factory())
.build())
.build()
.run(testId, editedMediaItem);
@ -2112,9 +2112,7 @@ public class TransformerEndToEndTest {
public void transmux_audioWithEditListUsingInAppMuxer_preservesDuration() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
Transformer transformer =
new Transformer.Builder(context)
.setMuxerFactory(new InAppMuxer.Factory.Builder().build())
.build();
new Transformer.Builder(context).setMuxerFactory(new InAppMp4Muxer.Factory()).build();
MediaItem mediaItem =
MediaItem.fromUri(Uri.parse("asset:///media/mp4/long_edit_list_audioonly.mp4"));
@ -2393,7 +2391,7 @@ public class TransformerEndToEndTest {
// The MediaMuxer is not writing the bitrate hence use the InAppMuxer.
Transformer transformer =
new Transformer.Builder(context)
.setMuxerFactory(new InAppMuxer.Factory.Builder().build())
.setMuxerFactory(new InAppMp4Muxer.Factory())
.setEncoderFactory(
new DefaultEncoderFactory.Builder(context)
.setRequestedAudioEncoderSettings(

View File

@ -38,9 +38,9 @@ import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
/** End-to-end instrumentation test for {@link Transformer} with {@link InAppMuxer}. */
/** End-to-end instrumentation test for {@link Transformer} with {@link InAppMp4Muxer}. */
@RunWith(Parameterized.class)
public class TransformerWithInAppMuxerEndToEndAndroidTest {
public class TransformerWithInAppMp4MuxerEndToEndAndroidTest {
private static final String MP4_FILE_ASSET_DIRECTORY = "asset:///media/mp4/";
private static final String H264_MP4 = "sample_no_bframes.mp4";
private static final String H265_MP4 = "h265_with_metadata_track.mp4";
@ -66,9 +66,7 @@ public class TransformerWithInAppMuxerEndToEndAndroidTest {
/* inputFormat= */ MP4_ASSET.videoFormat,
/* outputFormat= */ MP4_ASSET.videoFormat);
Transformer transformer =
new Transformer.Builder(context)
.setMuxerFactory(new InAppMuxer.Factory.Builder().build())
.build();
new Transformer.Builder(context).setMuxerFactory(new InAppMp4Muxer.Factory()).build();
ImmutableList<Effect> videoEffects = ImmutableList.of(RgbFilter.createGrayscaleFilter());
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_ASSET_DIRECTORY + inputFile));
EditedMediaItem editedMediaItem =
@ -91,9 +89,7 @@ public class TransformerWithInAppMuxerEndToEndAndroidTest {
assumeTrue(checkNotNull(inputFile).equals(H264_MP4));
String testId = "audioEditing_completesSuccessfully";
Transformer transformer =
new Transformer.Builder(context)
.setMuxerFactory(new InAppMuxer.Factory.Builder().build())
.build();
new Transformer.Builder(context).setMuxerFactory(new InAppMp4Muxer.Factory()).build();
ChannelMixingAudioProcessor channelMixingAudioProcessor = new ChannelMixingAudioProcessor();
channelMixingAudioProcessor.putChannelMixingMatrix(
ChannelMixingMatrix.create(/* inputChannelCount= */ 1, /* outputChannelCount= */ 2));

View File

@ -27,7 +27,7 @@ import androidx.media3.effect.RgbFilter;
import androidx.media3.transformer.EditedMediaItem;
import androidx.media3.transformer.Effects;
import androidx.media3.transformer.ExportTestResult;
import androidx.media3.transformer.InAppMuxer;
import androidx.media3.transformer.InAppMp4Muxer;
import androidx.media3.transformer.Transformer;
import androidx.media3.transformer.TransformerAndroidTestRunner;
import androidx.test.core.app.ApplicationProvider;
@ -40,9 +40,9 @@ import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
/** End-to-end instrumentation test for {@link Transformer} with {@link InAppMuxer}. */
/** End-to-end instrumentation test for {@link Transformer} with {@link InAppMp4Muxer}. */
@RunWith(AndroidJUnit4.class)
public class TransformerWithInAppMuxerEndToEndMhTest {
public class TransformerWithInAppMp4MuxerEndToEndMhTest {
@Rule public final TestName testName = new TestName();
private String testId;
@ -61,9 +61,7 @@ public class TransformerWithInAppMuxerEndToEndMhTest {
/* inputFormat= */ MP4_ASSET_AV1_VIDEO.videoFormat,
/* outputFormat= */ null);
Transformer transformer =
new Transformer.Builder(context)
.setMuxerFactory(new InAppMuxer.Factory.Builder().build())
.build();
new Transformer.Builder(context).setMuxerFactory(new InAppMp4Muxer.Factory()).build();
ImmutableList<Effect> videoEffects = ImmutableList.of(RgbFilter.createGrayscaleFilter());
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_ASSET_AV1_VIDEO.uri));
EditedMediaItem editedMediaItem =

View File

@ -0,0 +1,194 @@
/*
* Copyright 2023 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.transformer;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.Metadata;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.container.Mp4OrientationData;
import androidx.media3.muxer.FragmentedMp4Muxer;
import androidx.media3.muxer.Muxer;
import androidx.media3.muxer.MuxerException;
import androidx.media3.muxer.MuxerUtil;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.util.Locale;
/** {@link Muxer} implementation that uses a {@link FragmentedMp4Muxer}. */
// TODO: b/372417042 - Add E2E tests for producing fragmented MP4 output.
@UnstableApi
public final class InAppFragmentedMp4Muxer implements Muxer {
/** {@link Muxer.Factory} for {@link InAppFragmentedMp4Muxer}. */
public static final class Factory implements Muxer.Factory {
// TODO: b/372417042 - Move these lists to FragmentedMp4Muxer.
/** A list of supported video sample MIME types. */
private static final ImmutableList<String> SUPPORTED_VIDEO_SAMPLE_MIME_TYPES =
ImmutableList.of(
MimeTypes.VIDEO_AV1,
MimeTypes.VIDEO_H263,
MimeTypes.VIDEO_H264,
MimeTypes.VIDEO_H265,
MimeTypes.VIDEO_MP4V);
/** A list of supported audio sample MIME types. */
private static final ImmutableList<String> SUPPORTED_AUDIO_SAMPLE_MIME_TYPES =
ImmutableList.of(
MimeTypes.AUDIO_AAC,
MimeTypes.AUDIO_AMR_NB,
MimeTypes.AUDIO_AMR_WB,
MimeTypes.AUDIO_OPUS,
MimeTypes.AUDIO_VORBIS);
private final long fragmentDurationMs;
private long videoDurationUs;
/** Creates an instance with default values. */
public Factory() {
this(C.TIME_UNSET);
}
/**
* Creates an instance.
*
* @param fragmentDurationMs The fragment duration (in milliseconds).
*/
public Factory(long fragmentDurationMs) {
this.fragmentDurationMs = fragmentDurationMs;
videoDurationUs = C.TIME_UNSET;
}
/**
* Sets the duration of the video track (in microseconds) in the output.
*
* <p>Only the duration of the last sample is adjusted to achieve the given duration. Duration
* of the other samples remains unchanged.
*
* <p>The default is {@link C#TIME_UNSET} to not set any duration in the output. In this case
* the video track duration is determined by the samples written to it and the duration of the
* last sample will be same as that of the sample before that.
*
* @param videoDurationUs The duration of the video track (in microseconds) in the output, or
* {@link C#TIME_UNSET} to not set any duration. Only applicable when a video track is
* {@linkplain #addTrack(Format) added}.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setVideoDurationUs(long videoDurationUs) {
this.videoDurationUs = videoDurationUs;
return this;
}
@Override
public InAppFragmentedMp4Muxer create(String path) throws MuxerException {
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(path);
} catch (FileNotFoundException e) {
throw new MuxerException("Error creating file output stream", e);
}
FragmentedMp4Muxer.Builder builder = new FragmentedMp4Muxer.Builder(outputStream);
if (fragmentDurationMs != C.TIME_UNSET) {
builder.setFragmentDurationMs(fragmentDurationMs);
}
FragmentedMp4Muxer muxer = builder.build();
return new InAppFragmentedMp4Muxer(muxer, videoDurationUs);
}
@Override
public ImmutableList<String> getSupportedSampleMimeTypes(@C.TrackType int trackType) {
if (trackType == C.TRACK_TYPE_VIDEO) {
return SUPPORTED_VIDEO_SAMPLE_MIME_TYPES;
} else if (trackType == C.TRACK_TYPE_AUDIO) {
return SUPPORTED_AUDIO_SAMPLE_MIME_TYPES;
}
return ImmutableList.of();
}
}
private static final String TAG = "InAppFragmentedMp4Muxer";
private static final int TRACK_ID_UNSET = -1;
private final FragmentedMp4Muxer muxer;
private final long videoDurationUs;
private int videoTrackId;
private InAppFragmentedMp4Muxer(FragmentedMp4Muxer muxer, long videoDurationUs) {
this.muxer = muxer;
this.videoDurationUs = videoDurationUs;
videoTrackId = TRACK_ID_UNSET;
}
@Override
public int addTrack(Format format) {
int trackId = muxer.addTrack(format);
if (MimeTypes.isVideo(format.sampleMimeType)) {
muxer.addMetadataEntry(new Mp4OrientationData(format.rotationDegrees));
videoTrackId = trackId;
}
return trackId;
}
@Override
public void writeSampleData(int trackId, ByteBuffer byteBuffer, BufferInfo bufferInfo)
throws MuxerException {
if (videoDurationUs != C.TIME_UNSET
&& trackId == videoTrackId
&& bufferInfo.presentationTimeUs > videoDurationUs) {
Log.w(
TAG,
String.format(
Locale.US,
"Skipped sample with presentation time (%d) > video duration (%d)",
bufferInfo.presentationTimeUs,
videoDurationUs));
return;
}
muxer.writeSampleData(trackId, byteBuffer, bufferInfo);
}
@Override
public void addMetadataEntry(Metadata.Entry metadataEntry) {
if (MuxerUtil.isMetadataSupported(metadataEntry)) {
muxer.addMetadataEntry(metadataEntry);
}
}
@Override
public void close() throws MuxerException {
if (videoDurationUs != C.TIME_UNSET && videoTrackId != TRACK_ID_UNSET) {
BufferInfo bufferInfo = new BufferInfo();
bufferInfo.set(
/* newOffset= */ 0,
/* newSize= */ 0,
videoDurationUs,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
writeSampleData(videoTrackId, ByteBuffer.allocateDirect(0), bufferInfo);
}
muxer.close();
}
}

View File

@ -27,7 +27,6 @@ import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.container.Mp4OrientationData;
import androidx.media3.muxer.FragmentedMp4Muxer;
import androidx.media3.muxer.Mp4Muxer;
import androidx.media3.muxer.Muxer;
import androidx.media3.muxer.MuxerException;
@ -41,10 +40,9 @@ import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
/** {@link Muxer} implementation that uses an {@link Mp4Muxer} or {@link FragmentedMp4Muxer}. */
/** {@link Muxer} implementation that uses an {@link Mp4Muxer}. */
@UnstableApi
public final class InAppMuxer implements Muxer {
public final class InAppMp4Muxer implements Muxer {
/** Provides {@linkplain Metadata.Entry metadata} to add in the output MP4 file. */
public interface MetadataProvider {
@ -60,57 +58,9 @@ public final class InAppMuxer implements Muxer {
void updateMetadataEntries(Set<Metadata.Entry> metadataEntries);
}
/** {@link Muxer.Factory} for {@link InAppMuxer}. */
/** {@link Muxer.Factory} for {@link InAppMp4Muxer}. */
public static final class Factory implements Muxer.Factory {
/** A builder for {@link Factory} instances. */
public static final class Builder {
@Nullable private MetadataProvider metadataProvider;
private boolean outputFragmentedMp4;
private long fragmentDurationMs;
/** Creates a {@link Builder} instance with default values. */
public Builder() {
fragmentDurationMs = C.TIME_UNSET;
}
/**
* Sets an implementation of {@link MetadataProvider}.
*
* <p>The default value is {@code null}.
*
* <p>If the value is not set then the {@linkplain Metadata.Entry metadata} from the input
* file is set as it is in the output file.
*/
@CanIgnoreReturnValue
public Builder setMetadataProvider(MetadataProvider metadataProvider) {
this.metadataProvider = metadataProvider;
return this;
}
/** Sets whether to output a fragmented MP4. */
@CanIgnoreReturnValue
public Builder setOutputFragmentedMp4(boolean outputFragmentedMp4) {
this.outputFragmentedMp4 = outputFragmentedMp4;
return this;
}
/**
* Sets the fragment duration (in milliseconds) if the output file is {@link
* #setOutputFragmentedMp4(boolean) fragmented}.
*/
@CanIgnoreReturnValue
public Builder setFragmentDurationMs(long fragmentDurationMs) {
this.fragmentDurationMs = fragmentDurationMs;
return this;
}
/** Builds a {@link Factory} instance. */
public Factory build() {
return new Factory(metadataProvider, outputFragmentedMp4, fragmentDurationMs);
}
}
// TODO: b/372417042 - Move these lists to Mp4Muxer.
/** A list of supported video sample MIME types. */
private static final ImmutableList<String> SUPPORTED_VIDEO_SAMPLE_MIME_TYPES =
ImmutableList.of(
@ -130,18 +80,21 @@ public final class InAppMuxer implements Muxer {
MimeTypes.AUDIO_VORBIS);
@Nullable private final MetadataProvider metadataProvider;
private final boolean outputFragmentedMp4;
private final long fragmentDurationMs;
private long videoDurationUs;
private Factory(
@Nullable MetadataProvider metadataProvider,
boolean outputFragmentedMp4,
long fragmentDurationMs) {
/** Creates an instance with default values. */
public Factory() {
this(/* metadataProvider= */ null);
}
/**
* Creates an instance.
*
* @param metadataProvider A {@link MetadataProvider}.
*/
public Factory(@Nullable MetadataProvider metadataProvider) {
this.metadataProvider = metadataProvider;
this.outputFragmentedMp4 = outputFragmentedMp4;
this.fragmentDurationMs = fragmentDurationMs;
videoDurationUs = C.TIME_UNSET;
}
@ -167,7 +120,7 @@ public final class InAppMuxer implements Muxer {
}
@Override
public InAppMuxer create(String path) throws MuxerException {
public InAppMp4Muxer create(String path) throws MuxerException {
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(path);
@ -175,23 +128,14 @@ public final class InAppMuxer implements Muxer {
throw new MuxerException("Error creating file output stream", e);
}
Muxer muxer = null;
if (outputFragmentedMp4) {
FragmentedMp4Muxer.Builder builder = new FragmentedMp4Muxer.Builder(outputStream);
if (fragmentDurationMs != C.TIME_UNSET) {
builder.setFragmentDurationMs(fragmentDurationMs);
}
muxer = builder.build();
} else {
Mp4Muxer.Builder builder = new Mp4Muxer.Builder(outputStream);
if (videoDurationUs != C.TIME_UNSET) {
builder.setLastSampleDurationBehavior(
LAST_SAMPLE_DURATION_BEHAVIOR_SET_FROM_END_OF_STREAM_BUFFER_OR_DUPLICATE_PREVIOUS);
}
muxer = builder.build();
Mp4Muxer.Builder builder = new Mp4Muxer.Builder(outputStream);
if (videoDurationUs != C.TIME_UNSET) {
builder.setLastSampleDurationBehavior(
LAST_SAMPLE_DURATION_BEHAVIOR_SET_FROM_END_OF_STREAM_BUFFER_OR_DUPLICATE_PREVIOUS);
}
Mp4Muxer muxer = builder.build();
return new InAppMuxer(muxer, metadataProvider, videoDurationUs);
return new InAppMp4Muxer(muxer, metadataProvider, videoDurationUs);
}
@Override
@ -205,18 +149,18 @@ public final class InAppMuxer implements Muxer {
}
}
private static final String TAG = "InAppMuxer";
private static final String TAG = "InAppMp4Muxer";
private static final int TRACK_ID_UNSET = -1;
private final Muxer muxer;
private final Mp4Muxer muxer;
@Nullable private final MetadataProvider metadataProvider;
private final long videoDurationUs;
private final Set<Metadata.Entry> metadataEntries;
private int videoTrackId;
private InAppMuxer(
Muxer muxer, @Nullable MetadataProvider metadataProvider, long videoDurationUs) {
private InAppMp4Muxer(
Mp4Muxer muxer, @Nullable MetadataProvider metadataProvider, long videoDurationUs) {
this.muxer = muxer;
this.metadataProvider = metadataProvider;
this.videoDurationUs = videoDurationUs;

View File

@ -66,7 +66,7 @@ public final class EncodedSampleExporterTest {
new TransformationRequest.Builder().build(),
new MuxerWrapper(
/* outputPath= */ "unused",
new InAppMuxer.Factory.Builder().build(),
new InAppMp4Muxer.Factory(),
mock(MuxerWrapper.Listener.class),
MuxerWrapper.MUXER_MODE_DEFAULT,
/* dropSamplesBeforeFirstVideoSample= */ false,

View File

@ -39,9 +39,9 @@ import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** End to end parameterized tests for {@link Transformer} with {@link InAppMuxer}. */
/** End to end parameterized tests for {@link Transformer} with {@link InAppMp4Muxer}. */
@RunWith(ParameterizedRobolectricTestRunner.class)
public class TransformerWithInAppMuxerEndToEndParameterizedTest {
public class TransformerWithInAppMp4MuxerEndToEndParameterizedTest {
private static final String H263_3GP = "mp4/bbb_176x144_128kbps_15fps_h263.3gp";
private static final String H264_MP4 = "mp4/sample_no_bframes.mp4";
@ -84,15 +84,13 @@ public class TransformerWithInAppMuxerEndToEndParameterizedTest {
@Test
public void transmux_mp4File_outputMatchesExpected() throws Exception {
Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory.Builder()
.setMetadataProvider(
metadataEntries ->
// Add timestamp to make output file deterministic.
metadataEntries.add(
new Mp4TimestampData(
/* creationTimestampSeconds= */ 3_000_000_000L,
/* modificationTimestampSeconds= */ 4_000_000_000L)))
.build();
new InAppMp4Muxer.Factory(
metadataEntries ->
// Add timestamp to make output file deterministic.
metadataEntries.add(
new Mp4TimestampData(
/* creationTimestampSeconds= */ 3_000_000_000L,
/* modificationTimestampSeconds= */ 4_000_000_000L)));
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();

View File

@ -47,9 +47,9 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
/** End-to-end test for {@link Transformer} with {@link InAppMuxer}. */
/** End-to-end test for {@link Transformer} with {@link InAppMp4Muxer}. */
@RunWith(AndroidJUnit4.class)
public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
public class TransformerWithInAppMp4MuxerEndToEndTest {
private static final String MP4_FILE_PATH = "asset:///media/mp4/sample_no_bframes.mp4";
@Rule public final TemporaryFolder outputDir = new TemporaryFolder();
@ -67,15 +67,13 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
String tsFilePath = "asset:///media/ts/sample_h264.ts";
String tsFileName = "ts/sample_h264.ts";
Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory.Builder()
.setMetadataProvider(
metadataEntries ->
// Add timestamp to make output file deterministic.
metadataEntries.add(
new Mp4TimestampData(
/* creationTimestampSeconds= */ 3_000_000_000L,
/* modificationTimestampSeconds= */ 4_000_000_000L)))
.build();
new InAppMp4Muxer.Factory(
metadataEntries ->
// Add timestamp to make output file deterministic.
metadataEntries.add(
new Mp4TimestampData(
/* creationTimestampSeconds= */ 3_000_000_000L,
/* modificationTimestampSeconds= */ 4_000_000_000L)));
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
@ -104,14 +102,11 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
Mp4LocationData expectedLocationData =
new Mp4LocationData(/* latitude= */ 45f, /* longitude= */ -90f);
Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory.Builder()
.setMetadataProvider(
metadataEntries -> {
metadataEntries.removeIf(
(Metadata.Entry entry) -> entry instanceof Mp4LocationData);
metadataEntries.add(expectedLocationData);
})
.build();
new InAppMp4Muxer.Factory(
metadataEntries -> {
metadataEntries.removeIf((Metadata.Entry entry) -> entry instanceof Mp4LocationData);
metadataEntries.add(expectedLocationData);
});
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH));
@ -130,9 +125,7 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
String xmpSampleData = "media/xmp/sample_datetime_xmp.xmp";
byte[] xmpData = androidx.media3.test.utils.TestUtil.getByteArray(context, xmpSampleData);
Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory.Builder()
.setMetadataProvider(metadataEntries -> metadataEntries.add(new XmpData(xmpData)))
.build();
new InAppMp4Muxer.Factory(metadataEntries -> metadataEntries.add(new XmpData(xmpData)));
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH));
@ -153,9 +146,7 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
/* value= */ Util.toByteArray(captureFps),
MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32);
Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory.Builder()
.setMetadataProvider(metadataEntries -> metadataEntries.add(expectedCaptureFps))
.build();
new InAppMp4Muxer.Factory(metadataEntries -> metadataEntries.add(expectedCaptureFps));
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH));
@ -181,9 +172,7 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
/* creationTimestampSeconds= */ 3_000_000_000L,
/* modificationTimestampSeconds= */ 4_000_000_000L);
Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory.Builder()
.setMetadataProvider(metadataEntries -> metadataEntries.add(expectedTimestampData))
.build();
new InAppMp4Muxer.Factory(metadataEntries -> metadataEntries.add(expectedTimestampData));
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
@ -212,13 +201,11 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
/* value= */ Util.toByteArray(600.0f),
MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32);
Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory.Builder()
.setMetadataProvider(
metadataEntries -> {
metadataEntries.add(expectedStringMetadata);
metadataEntries.add(expectedFloatMetadata);
})
.build();
new InAppMp4Muxer.Factory(
metadataEntries -> {
metadataEntries.add(expectedStringMetadata);
metadataEntries.add(expectedFloatMetadata);
});
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH));
@ -248,7 +235,7 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
@Test
public void transmux_withSettingVideoDuration_writesCorrectVideoDuration() throws Exception {
InAppMuxer.Factory inAppMuxerFactory = new InAppMuxer.Factory.Builder().build();
InAppMp4Muxer.Factory inAppMuxerFactory = new InAppMp4Muxer.Factory();
long expectedDurationUs = 2_000_000L;
inAppMuxerFactory.setVideoDurationUs(expectedDurationUs);
Transformer transformer =