diff --git a/libraries/test_data/src/test/assets/transformerdumps/testspecificdumps/writeSample_dropSamplesBeforeFirstVideoSampleEnabled_dropsAudioSamplesTimedBeforeFirstVideoSample/original.dump b/libraries/test_data/src/test/assets/transformerdumps/testspecificdumps/writeSample_dropSamplesBeforeFirstVideoSampleEnabled_dropsAudioSamplesTimedBeforeFirstVideoSample/original.dump index 55f04cf09a..d8d57e2d19 100644 --- a/libraries/test_data/src/test/assets/transformerdumps/testspecificdumps/writeSample_dropSamplesBeforeFirstVideoSampleEnabled_dropsAudioSamplesTimedBeforeFirstVideoSample/original.dump +++ b/libraries/test_data/src/test/assets/transformerdumps/testspecificdumps/writeSample_dropSamplesBeforeFirstVideoSampleEnabled_dropsAudioSamplesTimedBeforeFirstVideoSample/original.dump @@ -42,4 +42,4 @@ sample: size = 4 isKeyFrame = true presentationTimeUs = 15 -released = false +released = true diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java index e53aa2256e..62f404a6ee 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java @@ -78,6 +78,26 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Used for appending the remaining samples with the previously muxed partial file. */ public static final int MUXER_MODE_APPEND = 2; + /** Represents a reason for which the muxer is released. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + MUXER_RELEASE_REASON_COMPLETED, + MUXER_RELEASE_REASON_CANCELLED, + MUXER_RELEASE_REASON_ERROR + }) + public @interface MuxerReleaseReason {} + + /** Muxer is released after the export completed successfully. */ + public static final int MUXER_RELEASE_REASON_COMPLETED = 0; + + /** Muxer is released after the export was cancelled. */ + public static final int MUXER_RELEASE_REASON_CANCELLED = 1; + + /** Muxer is released after an error occurred during the export. */ + public static final int MUXER_RELEASE_REASON_ERROR = 2; + private static final String TIMER_THREAD_NAME = "Muxer:Timer"; private static final String MUXER_TIMEOUT_ERROR_FORMAT_STRING = "Abort: no output sample written in the last %d milliseconds. DebugTrace: %s"; @@ -515,19 +535,21 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * mode} to {@link #MUXER_MODE_APPEND}. In all other modes the {@link MuxerWrapper} cannot be used * anymore once this method has been called. * - * @param forCancellation Whether the reason for releasing the resources is the transformation - * cancellation. + *
The resources are always released when the {@code releaseReason} is {@link + * #MUXER_RELEASE_REASON_CANCELLED} or {@link #MUXER_RELEASE_REASON_ERROR}. + * + * @param releaseReason The reason to release the muxer. * @throws Muxer.MuxerException If the underlying {@link Muxer} fails to finish writing the output - * and {@code forCancellation} is false. + * and the {@code releaseReason} is not {@link #MUXER_RELEASE_REASON_CANCELLED}. */ - public void release(boolean forCancellation) throws Muxer.MuxerException { - if (muxerMode == MUXER_MODE_MUX_PARTIAL && !forCancellation) { + public void release(@MuxerReleaseReason int releaseReason) throws Muxer.MuxerException { + if (releaseReason == MUXER_RELEASE_REASON_COMPLETED && muxerMode == MUXER_MODE_MUX_PARTIAL) { return; } isReady = false; abortScheduledExecutorService.shutdownNow(); if (muxer != null) { - muxer.release(forCancellation); + muxer.release(releaseReason == MUXER_RELEASE_REASON_CANCELLED); } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java index fbe1ad3722..b7d28f85fe 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerInternal.java @@ -27,6 +27,9 @@ import static androidx.media3.transformer.Composition.HDR_MODE_TONE_MAP_HDR_TO_S import static androidx.media3.transformer.ExoAssetLoaderVideoRenderer.getDecoderOutputColor; import static androidx.media3.transformer.ExportException.ERROR_CODE_FAILED_RUNTIME_CHECK; import static androidx.media3.transformer.ExportException.ERROR_CODE_MUXING_FAILED; +import static androidx.media3.transformer.MuxerWrapper.MUXER_RELEASE_REASON_CANCELLED; +import static androidx.media3.transformer.MuxerWrapper.MUXER_RELEASE_REASON_COMPLETED; +import static androidx.media3.transformer.MuxerWrapper.MUXER_RELEASE_REASON_ERROR; import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE; import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED; import static androidx.media3.transformer.TransformerUtil.getProcessedTrackType; @@ -403,7 +406,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } try { - muxerWrapper.release(forCancellation); + muxerWrapper.release(getMuxerReleaseReason(endReason)); } catch (Muxer.MuxerException e) { if (releaseExportException == null) { releaseExportException = ExportException.createForMuxer(e, ERROR_CODE_MUXING_FAILED); @@ -461,6 +464,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } + private @MuxerWrapper.MuxerReleaseReason int getMuxerReleaseReason( + @EndReason int exportEndReason) { + if (exportEndReason == END_REASON_COMPLETED) { + return MUXER_RELEASE_REASON_COMPLETED; + } else if (exportEndReason == END_REASON_CANCELLED) { + return MUXER_RELEASE_REASON_CANCELLED; + } else if (exportEndReason == END_REASON_ERROR) { + return MUXER_RELEASE_REASON_ERROR; + } + throw new IllegalStateException("Unexpected end reason " + exportEndReason); + } + private void updateProgressInternal() { if (released) { return; diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/MuxerWrapperTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/MuxerWrapperTest.java index 49a5811530..9ac23a83c4 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/MuxerWrapperTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/MuxerWrapperTest.java @@ -69,7 +69,8 @@ public class MuxerWrapperTest { @After public void tearDown() throws Muxer.MuxerException { if (muxerWrapper != null) { - muxerWrapper.release(/* forCancellation= */ false); + // Release with reason cancellation so that underlying resources are always released. + muxerWrapper.release(MuxerWrapper.MUXER_RELEASE_REASON_CANCELLED); } } @@ -136,6 +137,7 @@ public class MuxerWrapperTest { muxerWrapper.writeSample( C.TRACK_TYPE_VIDEO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 0); muxerWrapper.endTrack(C.TRACK_TYPE_VIDEO); + muxerWrapper.release(MuxerWrapper.MUXER_RELEASE_REASON_COMPLETED); muxerWrapper.changeToAppendMode(); muxerWrapper.setTrackCount(1); @@ -156,6 +158,7 @@ public class MuxerWrapperTest { muxerWrapper.writeSample( C.TRACK_TYPE_AUDIO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 0); muxerWrapper.endTrack(C.TRACK_TYPE_AUDIO); + muxerWrapper.release(MuxerWrapper.MUXER_RELEASE_REASON_COMPLETED); muxerWrapper.changeToAppendMode(); muxerWrapper.setTrackCount(1); @@ -176,6 +179,7 @@ public class MuxerWrapperTest { muxerWrapper.writeSample( C.TRACK_TYPE_VIDEO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 0); muxerWrapper.endTrack(C.TRACK_TYPE_VIDEO); + muxerWrapper.release(MuxerWrapper.MUXER_RELEASE_REASON_COMPLETED); muxerWrapper.changeToAppendMode(); muxerWrapper.setTrackCount(1); Format differentVideoFormat = FAKE_VIDEO_TRACK_FORMAT.buildUpon().setHeight(5000).build(); @@ -198,6 +202,7 @@ public class MuxerWrapperTest { muxerWrapper.writeSample( C.TRACK_TYPE_AUDIO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 0); muxerWrapper.endTrack(C.TRACK_TYPE_AUDIO); + muxerWrapper.release(MuxerWrapper.MUXER_RELEASE_REASON_COMPLETED); muxerWrapper.changeToAppendMode(); muxerWrapper.setTrackCount(1); Format differentAudioFormat = FAKE_AUDIO_TRACK_FORMAT.buildUpon().setSampleRate(48000).build(); @@ -264,6 +269,8 @@ public class MuxerWrapperTest { C.TRACK_TYPE_AUDIO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 17); muxerWrapper.endTrack(C.TRACK_TYPE_AUDIO); muxerWrapper.endTrack(C.TRACK_TYPE_VIDEO); + muxerWrapper.release(MuxerWrapper.MUXER_RELEASE_REASON_COMPLETED); + muxerWrapper = null; DumpFileAsserts.assertOutput( context, @@ -285,8 +292,10 @@ public class MuxerWrapperTest { muxerWrapper.writeSample( C.TRACK_TYPE_VIDEO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 0); muxerWrapper.endTrack(C.TRACK_TYPE_VIDEO); + muxerWrapper.release(MuxerWrapper.MUXER_RELEASE_REASON_COMPLETED); assertThat(muxerWrapper.isEnded()).isTrue(); + muxerWrapper = null; } @Test @@ -311,8 +320,10 @@ public class MuxerWrapperTest { muxerWrapper.writeSample( C.TRACK_TYPE_AUDIO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 0); muxerWrapper.endTrack(C.TRACK_TYPE_AUDIO); + muxerWrapper.release(MuxerWrapper.MUXER_RELEASE_REASON_COMPLETED); assertThat(muxerWrapper.isEnded()).isTrue(); + muxerWrapper = null; } @Test @@ -329,6 +340,7 @@ public class MuxerWrapperTest { muxerWrapper.writeSample( C.TRACK_TYPE_VIDEO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 0); muxerWrapper.endTrack(C.TRACK_TYPE_VIDEO); + muxerWrapper.release(MuxerWrapper.MUXER_RELEASE_REASON_COMPLETED); muxerWrapper.changeToAppendMode(); muxerWrapper.setTrackCount(1); muxerWrapper.addTrackFormat(FAKE_VIDEO_TRACK_FORMAT); @@ -437,6 +449,80 @@ public class MuxerWrapperTest { assertThat(MuxerWrapper.isInitializationDataCompatible(existingFormat, otherFormat)).isFalse(); } + @Test + public void release_withReasonCompletedInMuxPartialMode_doesNotReleaseResources() + throws Exception { + muxerWrapper = + new MuxerWrapper( + temporaryFolder.newFile().getPath(), + new DefaultMuxer.Factory(), + new NoOpMuxerListenerImpl(), + MUXER_MODE_MUX_PARTIAL, + /* dropSamplesBeforeFirstVideoSample= */ false); + muxerWrapper.setTrackCount(1); + muxerWrapper.addTrackFormat(FAKE_VIDEO_TRACK_FORMAT); + muxerWrapper.writeSample( + C.TRACK_TYPE_VIDEO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 0); + + muxerWrapper.release(MuxerWrapper.MUXER_RELEASE_REASON_COMPLETED); + + // Resources are not released and samples can be written in the append mode. + muxerWrapper.changeToAppendMode(); + boolean sampleWritten = + muxerWrapper.writeSample( + C.TRACK_TYPE_VIDEO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 100); + assertThat(sampleWritten).isTrue(); + } + + @Test + public void release_withReleaseReasonCancelledInMuxPartialMode_releasesResources() + throws Exception { + muxerWrapper = + new MuxerWrapper( + temporaryFolder.newFile().getPath(), + new DefaultMuxer.Factory(), + new NoOpMuxerListenerImpl(), + MUXER_MODE_MUX_PARTIAL, + /* dropSamplesBeforeFirstVideoSample= */ false); + muxerWrapper.setTrackCount(1); + muxerWrapper.addTrackFormat(FAKE_VIDEO_TRACK_FORMAT); + muxerWrapper.writeSample( + C.TRACK_TYPE_VIDEO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 0); + + muxerWrapper.release(MuxerWrapper.MUXER_RELEASE_REASON_CANCELLED); + + // Resources are released and samples can not be written in the append mode. + muxerWrapper.changeToAppendMode(); + boolean sampleWritten = + muxerWrapper.writeSample( + C.TRACK_TYPE_VIDEO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 100); + assertThat(sampleWritten).isFalse(); + } + + @Test + public void release_withReleaseReasonErrorInMuxPartialMode_releasesResources() throws Exception { + muxerWrapper = + new MuxerWrapper( + temporaryFolder.newFile().getPath(), + new DefaultMuxer.Factory(), + new NoOpMuxerListenerImpl(), + MUXER_MODE_MUX_PARTIAL, + /* dropSamplesBeforeFirstVideoSample= */ false); + muxerWrapper.setTrackCount(1); + muxerWrapper.addTrackFormat(FAKE_VIDEO_TRACK_FORMAT); + muxerWrapper.writeSample( + C.TRACK_TYPE_VIDEO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 0); + + muxerWrapper.release(MuxerWrapper.MUXER_RELEASE_REASON_ERROR); + + // Resources are released and samples can not be written in the append mode. + muxerWrapper.changeToAppendMode(); + boolean sampleWritten = + muxerWrapper.writeSample( + C.TRACK_TYPE_VIDEO, FAKE_SAMPLE, /* isKeyFrame= */ true, /* presentationTimeUs= */ 100); + assertThat(sampleWritten).isFalse(); + } + private static final class NoOpMuxerListenerImpl implements MuxerWrapper.Listener { @Override