Abandon trim optimization when transcoding effects are set

PiperOrigin-RevId: 588839072
This commit is contained in:
tofunmi 2023-12-07 10:36:47 -08:00 committed by Copybara-Service
parent ab798659d9
commit c5c8e988e8
6 changed files with 132 additions and 29 deletions

View File

@ -28,9 +28,11 @@ import static androidx.media3.transformer.AndroidTestUtil.PNG_ASSET_URI_STRING;
import static androidx.media3.transformer.AndroidTestUtil.createOpenGlObjects; import static androidx.media3.transformer.AndroidTestUtil.createOpenGlObjects;
import static androidx.media3.transformer.AndroidTestUtil.generateTextureFromBitmap; import static androidx.media3.transformer.AndroidTestUtil.generateTextureFromBitmap;
import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped; import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped;
import static androidx.media3.transformer.ExportResult.OPTIMIZATION_ABANDONED;
import static androidx.media3.transformer.ExportResult.OPTIMIZATION_SUCCEEDED; import static androidx.media3.transformer.ExportResult.OPTIMIZATION_SUCCEEDED;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue; import static org.junit.Assume.assumeTrue;
import android.content.Context; import android.content.Context;
@ -478,6 +480,38 @@ public class TransformerEndToEndTest {
assertThat(result.exportResult.durationMs).isAtMost(2000); assertThat(result.exportResult.durationMs).isAtMost(2000);
} }
@Test
public void videoEditing_trimOptimizationEnabled_fallbackToNormalExport() throws Exception {
String testId = "videoEditing_trimOptimizationEnabled_fallbackToNormalExport";
Transformer transformer =
new Transformer.Builder(context).experimentalSetTrimOptimizationEnabled(true).build();
// The trim optimization is only guaranteed to work on emulator for this file.
assumeTrue(isRunningOnEmulator());
// MediaCodec returns a segmentation fault fails at this SDK level on emulators.
assumeFalse(Util.SDK_INT == 26);
MediaItem mediaItem =
new MediaItem.Builder()
.setUri("asset:///media/mp4/crow_emulator_transformer_output.mp4")
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(500)
.setEndPositionMs(2500)
.build())
.build();
ImmutableList<Effect> videoEffects = ImmutableList.of(Presentation.createForHeight(480));
EditedMediaItem editedMediaItem =
new EditedMediaItem.Builder(mediaItem)
.setEffects(new Effects(/* audioProcessors= */ ImmutableList.of(), videoEffects))
.build();
ExportTestResult result =
new TransformerAndroidTestRunner.Builder(context, transformer)
.build()
.run(testId, editedMediaItem);
assertThat(result.exportResult.optimizationResult).isEqualTo(OPTIMIZATION_ABANDONED);
}
@Test @Test
public void videoEncoderFormatUnsupported_completesWithError() throws Exception { public void videoEncoderFormatUnsupported_completesWithError() throws Exception {
String testId = "videoEncoderFormatUnsupported_completesWithError"; String testId = "videoEncoderFormatUnsupported_completesWithError";

View File

@ -64,15 +64,20 @@ import org.checkerframework.checker.nullness.qual.Nullable;
/** The video {@link Format} or {@code null} if there is no video track. */ /** The video {@link Format} or {@code null} if there is no video track. */
public final @Nullable Format videoFormat; public final @Nullable Format videoFormat;
/** The audio {@link Format} or {@code null} if there is no audio track. */
public final @Nullable Format audioFormat;
private Mp4MetadataInfo( private Mp4MetadataInfo(
long durationUs, long durationUs,
long lastSyncSampleTimestampUs, long lastSyncSampleTimestampUs,
long firstSyncSampleTimestampUsAfterTimeUs, long firstSyncSampleTimestampUsAfterTimeUs,
@Nullable Format videoFormat) { @Nullable Format videoFormat,
@Nullable Format audioFormat) {
this.durationUs = durationUs; this.durationUs = durationUs;
this.lastSyncSampleTimestampUs = lastSyncSampleTimestampUs; this.lastSyncSampleTimestampUs = lastSyncSampleTimestampUs;
this.firstSyncSampleTimestampUsAfterTimeUs = firstSyncSampleTimestampUsAfterTimeUs; this.firstSyncSampleTimestampUsAfterTimeUs = firstSyncSampleTimestampUsAfterTimeUs;
this.videoFormat = videoFormat; this.videoFormat = videoFormat;
this.audioFormat = audioFormat;
} }
/** /**
@ -129,11 +134,11 @@ import org.checkerframework.checker.nullness.qual.Nullable;
throw new IllegalStateException("The MP4 file is invalid"); throw new IllegalStateException("The MP4 file is invalid");
} }
} }
long durationUs = mp4Extractor.getDurationUs(); long durationUs = mp4Extractor.getDurationUs();
long lastSyncSampleTimestampUs = C.TIME_UNSET; long lastSyncSampleTimestampUs = C.TIME_UNSET;
long firstSyncSampleTimestampUsAfterTimeUs = C.TIME_UNSET; long firstSyncSampleTimestampUsAfterTimeUs = C.TIME_UNSET;
@Nullable Format videoFormat = null; @Nullable Format videoFormat = null;
if (extractorOutput.videoTrackId != C.INDEX_UNSET) { if (extractorOutput.videoTrackId != C.INDEX_UNSET) {
ExtractorOutputImpl.TrackOutputImpl videoTrackOutput = ExtractorOutputImpl.TrackOutputImpl videoTrackOutput =
checkNotNull(extractorOutput.trackTypeToTrackOutput.get(C.TRACK_TYPE_VIDEO)); checkNotNull(extractorOutput.trackTypeToTrackOutput.get(C.TRACK_TYPE_VIDEO));
@ -155,11 +160,20 @@ import org.checkerframework.checker.nullness.qual.Nullable;
} }
} }
} }
@Nullable Format audioFormat = null;
if (extractorOutput.audioTrackId != C.INDEX_UNSET) {
ExtractorOutputImpl.TrackOutputImpl audioTrackOutput =
checkNotNull(extractorOutput.trackTypeToTrackOutput.get(C.TRACK_TYPE_AUDIO));
audioFormat = checkNotNull(audioTrackOutput.format);
}
return new Mp4MetadataInfo( return new Mp4MetadataInfo(
durationUs, durationUs,
lastSyncSampleTimestampUs, lastSyncSampleTimestampUs,
firstSyncSampleTimestampUsAfterTimeUs, firstSyncSampleTimestampUsAfterTimeUs,
videoFormat); videoFormat,
audioFormat);
} finally { } finally {
DataSourceUtil.closeQuietly(dataSource); DataSourceUtil.closeQuietly(dataSource);
mp4Extractor.release(); mp4Extractor.release();
@ -168,12 +182,14 @@ import org.checkerframework.checker.nullness.qual.Nullable;
private static final class ExtractorOutputImpl implements ExtractorOutput { private static final class ExtractorOutputImpl implements ExtractorOutput {
public int videoTrackId; public int videoTrackId;
public int audioTrackId;
public boolean seekMapInitialized; public boolean seekMapInitialized;
final Map<Integer, TrackOutputImpl> trackTypeToTrackOutput; final Map<Integer, TrackOutputImpl> trackTypeToTrackOutput;
public ExtractorOutputImpl() { public ExtractorOutputImpl() {
videoTrackId = C.INDEX_UNSET; videoTrackId = C.INDEX_UNSET;
audioTrackId = C.INDEX_UNSET;
trackTypeToTrackOutput = new HashMap<>(); trackTypeToTrackOutput = new HashMap<>();
} }
@ -181,6 +197,8 @@ import org.checkerframework.checker.nullness.qual.Nullable;
public TrackOutput track(int id, @C.TrackType int type) { public TrackOutput track(int id, @C.TrackType int type) {
if (type == C.TRACK_TYPE_VIDEO) { if (type == C.TRACK_TYPE_VIDEO) {
videoTrackId = id; videoTrackId = id;
} else if (type == C.TRACK_TYPE_AUDIO) {
audioTrackId = id;
} }
@Nullable TrackOutputImpl trackOutput = trackTypeToTrackOutput.get(type); @Nullable TrackOutputImpl trackOutput = trackTypeToTrackOutput.get(type);

View File

@ -22,6 +22,8 @@ import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.transformer.Composition.HDR_MODE_EXPERIMENTAL_FORCE_INTERPRET_HDR_AS_SDR; import static androidx.media3.transformer.Composition.HDR_MODE_EXPERIMENTAL_FORCE_INTERPRET_HDR_AS_SDR;
import static androidx.media3.transformer.ExportResult.OPTIMIZATION_ABANDONED; import static androidx.media3.transformer.ExportResult.OPTIMIZATION_ABANDONED;
import static androidx.media3.transformer.ExportResult.OPTIMIZATION_FAILED_EXTRACTION_FAILED; import static androidx.media3.transformer.ExportResult.OPTIMIZATION_FAILED_EXTRACTION_FAILED;
import static androidx.media3.transformer.TransformerUtil.shouldTranscodeAudio;
import static androidx.media3.transformer.TransformerUtil.shouldTranscodeVideo;
import static androidx.media3.transformer.TransmuxTranscodeHelper.buildNewCompositionWithClipTimes; import static androidx.media3.transformer.TransmuxTranscodeHelper.buildNewCompositionWithClipTimes;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
@ -301,14 +303,13 @@ public final class Transformer {
* <li>Progress updates will be unavailable. * <li>Progress updates will be unavailable.
* </ul> * </ul>
* *
* <p>{@link ExportResult#optimizationResult} will indicate whether the optimization was
* applied.
*
* <p>This process relies on the given {@linkplain #setEncoderFactory EncoderFactory} providing * <p>This process relies on the given {@linkplain #setEncoderFactory EncoderFactory} providing
* the right encoder level and profiles when transcoding, so that the transcoded and transmuxed * the right encoder level and profiles when transcoding, so that the transcoded and transmuxed
* segments of the file can be stitched together. If the file segments can't be stitched * segments of the file can be stitched together. If the file segments can't be stitched
* together, the {@linkplain #start(Composition, String) export operation} will throw an * together, Transformer throw away any progress and proceed with unoptimized export instead.
* exception. *
* <p>The {@link ExportResult#optimizationResult} will indicate whether the optimization was
* applied.
* *
* @param enabled Whether to enable trim optimization. * @param enabled Whether to enable trim optimization.
* @return This builder. * @return This builder.
@ -1257,7 +1258,32 @@ public final class Transformer {
processFullInput(); processFullInput();
return; return;
} }
remuxingMuxerWrapper =
new MuxerWrapper(
checkNotNull(outputFilePath),
muxerFactory,
componentListener,
MuxerWrapper.MUXER_MODE_MUX_PARTIAL);
if (shouldTranscodeVideo(
checkNotNull(mp4MetadataInfo.videoFormat),
composition,
/* sequenceIndex= */ 0,
transformationRequest,
encoderFactory,
remuxingMuxerWrapper)
|| (mp4MetadataInfo.audioFormat != null
&& shouldTranscodeAudio(
mp4MetadataInfo.audioFormat,
composition,
/* sequenceIndex= */ 0,
transformationRequest,
encoderFactory,
remuxingMuxerWrapper))) {
remuxingMuxerWrapper = null;
exportResultBuilder.setOptimizationResult(OPTIMIZATION_ABANDONED);
processFullInput();
return;
}
Transformer.this.mp4MetadataInfo = mp4MetadataInfo; Transformer.this.mp4MetadataInfo = mp4MetadataInfo;
Composition trancodeComposition = Composition trancodeComposition =
buildNewCompositionWithClipTimes( buildNewCompositionWithClipTimes(
@ -1267,17 +1293,9 @@ public final class Transformer {
mp4MetadataInfo.durationUs, mp4MetadataInfo.durationUs,
/* startsAtKeyFrame= */ false); /* startsAtKeyFrame= */ false);
// TODO: b/304476154 - Check for cases where we shouldTranscode anyway and proceed with
// normal export instead.
remuxingMuxerWrapper =
new MuxerWrapper(
checkNotNull(outputFilePath),
muxerFactory,
componentListener,
MuxerWrapper.MUXER_MODE_MUX_PARTIAL);
startInternal( startInternal(
trancodeComposition, trancodeComposition,
remuxingMuxerWrapper, checkNotNull(remuxingMuxerWrapper),
componentListener, componentListener,
/* initialTimestampOffsetUs= */ 0); /* initialTimestampOffsetUs= */ 0);
} }

View File

@ -45,6 +45,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.DebugViewProvider; import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.VideoFrameProcessor; import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
@ -692,12 +693,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} else if (trackType == C.TRACK_TYPE_VIDEO) { } else if (trackType == C.TRACK_TYPE_VIDEO) {
shouldTranscode = shouldTranscode =
shouldTranscodeVideo( shouldTranscodeVideo(
inputFormat, inputFormat,
composition, composition,
sequenceIndex, sequenceIndex,
transformationRequest, transformationRequest,
encoderFactory, encoderFactory,
muxerWrapper); muxerWrapper)
|| clippingRequiresTranscode(firstEditedMediaItem.mediaItem);
} }
checkState(!shouldTranscode || assetLoaderCanOutputDecoded); checkState(!shouldTranscode || assetLoaderCanOutputDecoded);
@ -706,6 +708,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
private static boolean clippingRequiresTranscode(MediaItem mediaItem) {
return mediaItem.clippingConfiguration.startPositionMs > 0
&& !mediaItem.clippingConfiguration.startsAtKeyFrame;
}
/** Tracks the inputs and outputs of {@link AssetLoader AssetLoaders}. */ /** Tracks the inputs and outputs of {@link AssetLoader AssetLoaders}. */
private static final class AssetLoaderInputTracker { private static final class AssetLoaderInputTracker {
private final List<SequenceMetadata> sequencesMetadata; private final List<SequenceMetadata> sequencesMetadata;

View File

@ -125,10 +125,6 @@ import com.google.common.collect.ImmutableList;
} }
EditedMediaItem firstEditedMediaItem = EditedMediaItem firstEditedMediaItem =
composition.sequences.get(sequenceIndex).editedMediaItems.get(0); composition.sequences.get(sequenceIndex).editedMediaItems.get(0);
if (firstEditedMediaItem.mediaItem.clippingConfiguration.startPositionMs > 0
&& !firstEditedMediaItem.mediaItem.clippingConfiguration.startsAtKeyFrame) {
return true;
}
if (encoderFactory.videoNeedsEncoding()) { if (encoderFactory.videoNeedsEncoding()) {
return true; return true;
} }

View File

@ -15,6 +15,8 @@
*/ */
package androidx.media3.transformer; package androidx.media3.transformer;
import static androidx.media3.common.MimeTypes.AUDIO_AAC;
import static androidx.media3.common.MimeTypes.VIDEO_H264;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
@ -125,7 +127,7 @@ public class Mp4MetadataInfoTest {
} }
@Test @Test
public void videoFormat_outputsFormatObjectWithCorrectInitializationData() throws IOException { public void videoFormat_outputsFormatObjectWithCorrectRelevantFormatData() throws IOException {
String mp4FilePath = "asset:///media/mp4/sample.mp4"; String mp4FilePath = "asset:///media/mp4/sample.mp4";
Mp4MetadataInfo mp4MetadataInfo = Mp4MetadataInfo.create(context, mp4FilePath); Mp4MetadataInfo mp4MetadataInfo = Mp4MetadataInfo.create(context, mp4FilePath);
byte[] expectedCsd0 = { byte[] expectedCsd0 = {
@ -139,6 +141,11 @@ public class Mp4MetadataInfoTest {
assertThat(actualFormat).isNotNull(); assertThat(actualFormat).isNotNull();
assertThat(actualFormat.initializationData.get(0)).isEqualTo(expectedCsd0); assertThat(actualFormat.initializationData.get(0)).isEqualTo(expectedCsd0);
assertThat(actualFormat.initializationData.get(1)).isEqualTo(expectedCsd1); assertThat(actualFormat.initializationData.get(1)).isEqualTo(expectedCsd1);
assertThat(actualFormat.sampleMimeType).isEqualTo(VIDEO_H264);
assertThat(actualFormat.width).isEqualTo(1080);
assertThat(actualFormat.height).isEqualTo(720);
assertThat(actualFormat.rotationDegrees).isEqualTo(0);
assertThat(actualFormat.pixelWidthHeightRatio).isEqualTo(1);
} }
@Test @Test
@ -148,4 +155,27 @@ public class Mp4MetadataInfoTest {
assertThat(mp4MetadataInfo.videoFormat).isNull(); assertThat(mp4MetadataInfo.videoFormat).isNull();
} }
@Test
public void audioFormat_outputsFormatObjectWithCorrectRelevantFormatData() throws IOException {
String mp4FilePath = "asset:///media/mp4/sample.mp4";
Mp4MetadataInfo mp4MetadataInfo = Mp4MetadataInfo.create(context, mp4FilePath);
byte[] expectedCsd0 = {18, 8};
Format actualFormat = mp4MetadataInfo.audioFormat;
assertThat(actualFormat).isNotNull();
assertThat(actualFormat.sampleMimeType).isEqualTo(AUDIO_AAC);
assertThat(actualFormat.channelCount).isEqualTo(1);
assertThat(actualFormat.sampleRate).isEqualTo(44100);
assertThat(actualFormat.initializationData.get(0)).isEqualTo(expectedCsd0);
}
@Test
public void audioFormat_videoOnlyMp4File_outputsNull() throws IOException {
String mp4FilePath = "asset:///media/mp4/sample_18byte_nclx_colr.mp4";
Mp4MetadataInfo mp4MetadataInfo = Mp4MetadataInfo.create(context, mp4FilePath);
assertThat(mp4MetadataInfo.audioFormat).isNull();
}
} }