diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 82a9e41dbd..7950c9e542 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,9 @@ key names by modifying it to only check for hyphen ([#1028](https://github.com/androidx/media/issues/1028)). * Transformer: + * Add `audioConversionProcess` and `videoConversionProcess` to + `ExportResult` indicating how the respective track in the output file + was made. * Track Selection: * Extractors: * Audio: diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java index fd67bb1aac..e6063e66b0 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -30,6 +30,10 @@ import static androidx.media3.transformer.AndroidTestUtil.PNG_ASSET_URI_STRING; import static androidx.media3.transformer.AndroidTestUtil.createOpenGlObjects; import static androidx.media3.transformer.AndroidTestUtil.generateTextureFromBitmap; import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped; +import static androidx.media3.transformer.ExportResult.CONVERSION_PROCESS_NA; +import static androidx.media3.transformer.ExportResult.CONVERSION_PROCESS_TRANSCODED; +import static androidx.media3.transformer.ExportResult.CONVERSION_PROCESS_TRANSMUXED; +import static androidx.media3.transformer.ExportResult.CONVERSION_PROCESS_TRANSMUXED_AND_TRANSCODED; import static androidx.media3.transformer.ExportResult.OPTIMIZATION_ABANDONED_KEYFRAME_PLACEMENT_OPTIMAL_FOR_TRIM; import static androidx.media3.transformer.ExportResult.OPTIMIZATION_ABANDONED_TRIM_AND_TRANSCODING_TRANSFORMATION_REQUESTED; import static androidx.media3.transformer.ExportResult.OPTIMIZATION_FAILED_FORMAT_MISMATCH; @@ -530,6 +534,8 @@ public class TransformerEndToEndTest { assertThat(result.exportResult.optimizationResult) .isEqualTo(OPTIMIZATION_FAILED_FORMAT_MISMATCH); assertThat(result.exportResult.durationMs).isAtMost(clippingEndMs - clippingStartMs); + assertThat(result.exportResult.videoConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSCODED); + assertThat(result.exportResult.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); } @Test @@ -617,6 +623,8 @@ public class TransformerEndToEndTest { assertThat(result.exportResult.optimizationResult) .isEqualTo(OPTIMIZATION_ABANDONED_KEYFRAME_PLACEMENT_OPTIMAL_FOR_TRIM); assertThat(result.exportResult.durationMs).isAtMost(clippingEndMs - clippingStartMs); + assertThat(result.exportResult.videoConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSCODED); + assertThat(result.exportResult.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); } @Test @@ -653,6 +661,8 @@ public class TransformerEndToEndTest { .isEqualTo(OPTIMIZATION_ABANDONED_KEYFRAME_PLACEMENT_OPTIMAL_FOR_TRIM); // The asset is 15 s 537 ms long. assertThat(result.exportResult.durationMs).isAtMost(1_017); + assertThat(result.exportResult.videoConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSCODED); + assertThat(result.exportResult.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); } @Test @@ -685,6 +695,9 @@ public class TransformerEndToEndTest { assertThat(result.exportResult.optimizationResult).isEqualTo(OPTIMIZATION_SUCCEEDED); assertThat(result.exportResult.durationMs).isAtMost(2000); + assertThat(result.exportResult.videoConversionProcess) + .isEqualTo(CONVERSION_PROCESS_TRANSMUXED_AND_TRANSCODED); + assertThat(result.exportResult.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); } @Test @@ -726,6 +739,9 @@ public class TransformerEndToEndTest { assertThat(result.exportResult.optimizationResult).isEqualTo(OPTIMIZATION_SUCCEEDED); assertThat(result.exportResult.durationMs).isAtMost(2000); + assertThat(result.exportResult.videoConversionProcess) + .isEqualTo(CONVERSION_PROCESS_TRANSMUXED_AND_TRANSCODED); + assertThat(result.exportResult.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_NA); Format format = FileUtil.retrieveTrackFormat(context, result.filePath, C.TRACK_TYPE_VIDEO); // The video is trim-optimized, so the rotation is performed in MuxerWrapper. @@ -768,6 +784,8 @@ public class TransformerEndToEndTest { assertThat(result.exportResult.optimizationResult) .isEqualTo(OPTIMIZATION_ABANDONED_TRIM_AND_TRANSCODING_TRANSFORMATION_REQUESTED); + assertThat(result.exportResult.videoConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSCODED); + assertThat(result.exportResult.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); } @Test diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExportResult.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExportResult.java index a4e159a7df..4602e5858f 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ExportResult.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExportResult.java @@ -378,6 +378,39 @@ public final class ExportResult { */ public static final int OPTIMIZATION_FAILED_FORMAT_MISMATCH = 6; + /** + * Specifies what conversion process was used to make a track in the output file. One of: + * + * + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + CONVERSION_PROCESS_NA, + CONVERSION_PROCESS_TRANSCODED, + CONVERSION_PROCESS_TRANSMUXED, + CONVERSION_PROCESS_TRANSMUXED_AND_TRANSCODED + }) + @interface ConversionProcess {} + + /** The output file doesn't contain this track type. */ + public static final int CONVERSION_PROCESS_NA = 0; + + /** The track was transcoded. */ + public static final int CONVERSION_PROCESS_TRANSCODED = 1; + + /** The track was transmuxed. */ + public static final int CONVERSION_PROCESS_TRANSMUXED = 2; + + /** The track was both transcoded and transmuxed. */ + public static final int CONVERSION_PROCESS_TRANSMUXED_AND_TRANSCODED = 3; + /** The list of {@linkplain ProcessedInput processed inputs}. */ public final ImmutableList processedInputs; @@ -436,6 +469,12 @@ public final class ExportResult { */ @Nullable public final ExportException exportException; + /** Returns the {@link ConversionProcess} taken to create the video track in the output file. */ + public final @ConversionProcess int videoConversionProcess; + + /** Returns the {@link ConversionProcess} taken to create the audio track in the output file. */ + public final @ConversionProcess int audioConversionProcess; + private ExportResult( ImmutableList processedInputs, long durationMs, @@ -471,6 +510,12 @@ public final class ExportResult { this.videoMimeType = videoMimeType; this.optimizationResult = optimizationResult; this.exportException = exportException; + audioConversionProcess = + getConversionProcess( + audioMimeType, optimizationResult, processedInputs, C.TRACK_TYPE_AUDIO); + videoConversionProcess = + getConversionProcess( + videoMimeType, optimizationResult, processedInputs, C.TRACK_TYPE_VIDEO); } public Builder buildUpon() { @@ -543,4 +588,41 @@ public final class ExportResult { result = 31 * result + Objects.hashCode(exportException); return result; } + + // Nullness test incorrectly throws monotonic type error when assigning the @Nullable decoderName. + @SuppressWarnings("monotonic.type.incompatible") + private static @ConversionProcess int getConversionProcess( + @Nullable String mimeType, + @OptimizationResult int optimizationResult, + List processedInputs, + @C.TrackType int trackType) { + if (mimeType == null) { + return CONVERSION_PROCESS_NA; + } + if (optimizationResult == OPTIMIZATION_SUCCEEDED) { // Trim optimization occurred. + return trackType == C.TRACK_TYPE_AUDIO + ? CONVERSION_PROCESS_TRANSMUXED + : CONVERSION_PROCESS_TRANSMUXED_AND_TRANSCODED; + } + @ConversionProcess int conversionProcess = CONVERSION_PROCESS_NA; + for (ProcessedInput processedInput : processedInputs) { + @Nullable + String decoderName = + trackType == C.TRACK_TYPE_AUDIO + ? processedInput.audioDecoderName + : processedInput.videoDecoderName; + if (decoderName == null) { + if (conversionProcess == CONVERSION_PROCESS_TRANSCODED) { + return CONVERSION_PROCESS_TRANSMUXED_AND_TRANSCODED; + } + conversionProcess = CONVERSION_PROCESS_TRANSMUXED; + } else { + if (conversionProcess == CONVERSION_PROCESS_TRANSMUXED) { + return CONVERSION_PROCESS_TRANSMUXED_AND_TRANSCODED; + } + conversionProcess = CONVERSION_PROCESS_TRANSCODED; + } + } + return conversionProcess; + } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index 178e4ced3c..7d9c8b9cc9 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -1475,6 +1475,7 @@ public final class Transformer { if (!doesFormatsMatch(mediaItemInfo, firstEditedMediaItem)) { remuxingMuxerWrapper = null; transformerInternal = null; + exportResultBuilder.reset(); exportResultBuilder.setOptimizationResult(OPTIMIZATION_FAILED_FORMAT_MISMATCH); processFullInput(); return; diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java index c578b09405..1af86c9dc7 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java @@ -19,6 +19,8 @@ package androidx.media3.transformer; import static androidx.media3.test.utils.robolectric.RobolectricUtil.runLooperUntil; import static androidx.media3.transformer.AssetLoader.SUPPORTED_OUTPUT_TYPE_DECODED; import static androidx.media3.transformer.AssetLoader.SUPPORTED_OUTPUT_TYPE_ENCODED; +import static androidx.media3.transformer.ExportResult.CONVERSION_PROCESS_NA; +import static androidx.media3.transformer.ExportResult.CONVERSION_PROCESS_TRANSMUXED; import static androidx.media3.transformer.ExportResult.OPTIMIZATION_ABANDONED_KEYFRAME_PLACEMENT_OPTIMAL_FOR_TRIM; import static androidx.media3.transformer.ExportResult.OPTIMIZATION_FAILED_EXTRACTION_FAILED; import static androidx.media3.transformer.TestUtil.ASSET_URI_PREFIX; @@ -168,15 +170,17 @@ public final class MediaItemExportTest { .build(); transformer.start(mediaItem, outputDir.newFile().getPath()); - ExportResult exportResult = TransformerTestRunner.runLooper(transformer); + ExportResult result = TransformerTestRunner.runLooper(transformer); - assertThat(exportResult.optimizationResult) + assertThat(result.optimizationResult) .isEqualTo(OPTIMIZATION_ABANDONED_KEYFRAME_PLACEMENT_OPTIMAL_FOR_TRIM); // Asserts against file generated when experimentalSetTrimOptimizationEnabled is set to false. DumpFileAsserts.assertOutput( context, muxerFactory.getCreatedMuxer(), getDumpFileName(/* originalFileName= */ FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S)); + assertThat(result.videoConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); + assertThat(result.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); } @Test @@ -230,9 +234,9 @@ public final class MediaItemExportTest { .build(); transformer.start(mediaItem, outputDir.newFile().getPath()); - ExportResult exportResult = TransformerTestRunner.runLooper(transformer); + ExportResult result = TransformerTestRunner.runLooper(transformer); - assertThat(exportResult.optimizationResult) + assertThat(result.optimizationResult) .isEqualTo(OPTIMIZATION_ABANDONED_KEYFRAME_PLACEMENT_OPTIMAL_FOR_TRIM); DumpFileAsserts.assertOutput( context, @@ -240,6 +244,8 @@ public final class MediaItemExportTest { getDumpFileName( /* originalFileName= */ FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S, /* modifications...= */ "clipped")); + assertThat(result.videoConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); + assertThat(result.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); } @Test @@ -259,13 +265,15 @@ public final class MediaItemExportTest { .build(); transformer.start(mediaItem, outputDir.newFile().getPath()); - ExportResult exportResult = TransformerTestRunner.runLooper(transformer); + ExportResult result = TransformerTestRunner.runLooper(transformer); - assertThat(exportResult.optimizationResult).isEqualTo(OPTIMIZATION_FAILED_EXTRACTION_FAILED); + assertThat(result.optimizationResult).isEqualTo(OPTIMIZATION_FAILED_EXTRACTION_FAILED); DumpFileAsserts.assertOutput( context, muxerFactory.getCreatedMuxer(), getDumpFileName(/* originalFileName= */ FILE_AUDIO_RAW, /* modifications...= */ "clipped")); + assertThat(result.videoConversionProcess).isEqualTo(CONVERSION_PROCESS_NA); + assertThat(result.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); } @Test @@ -1069,11 +1077,13 @@ public final class MediaItemExportTest { new EditedMediaItem.Builder(mediaItem).setEffects(effects).build(); transformer.start(editedMediaItem, outputDir.newFile().getPath()); - TransformerTestRunner.runLooper(transformer); + ExportResult result = TransformerTestRunner.runLooper(transformer); // Video transcoding in unit tests is not supported. DumpFileAsserts.assertOutput( context, muxerFactory.getCreatedMuxer(), getDumpFileName(FILE_VIDEO_ONLY)); + assertThat(result.videoConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); + assertThat(result.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_NA); } @Test @@ -1089,7 +1099,7 @@ public final class MediaItemExportTest { new EditedMediaItem.Builder(mediaItem).setEffects(effects).build(); transformer.start(editedMediaItem, outputDir.newFile().getPath()); - TransformerTestRunner.runLooper(transformer); + ExportResult result = TransformerTestRunner.runLooper(transformer); // Video transcoding in unit tests is not supported. DumpFileAsserts.assertOutput( @@ -1097,6 +1107,8 @@ public final class MediaItemExportTest { muxerFactory.getCreatedMuxer(), getDumpFileName( /* originalFileName= */ FILE_AUDIO_VIDEO, /* modifications...= */ "rotated")); + assertThat(result.videoConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); + assertThat(result.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED); } @Test