diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2c6be59899..3f74bd7298 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,9 @@ * Effect: * Improved PQ to SDR tone-mapping by converting color spaces. +* Transformer: + * Add workaround for exception thrown due to `MediaMuxer` not supporting + negative presentation timestamps before API 30. * UI: * Fallback to include audio track language name if `Locale` cannot identify a display name diff --git a/libraries/test_data/src/test/assets/media/mp4/iibbibb_editlist_videoonly.mp4 b/libraries/test_data/src/test/assets/media/mp4/iibbibb_editlist_videoonly.mp4 new file mode 100644 index 0000000000..2dd1701472 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mp4/iibbibb_editlist_videoonly.mp4 differ diff --git a/libraries/test_data/src/test/assets/media/mp4/long_edit_list_audioonly.mp4 b/libraries/test_data/src/test/assets/media/mp4/long_edit_list_audioonly.mp4 new file mode 100644 index 0000000000..bf78e9d4d1 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mp4/long_edit_list_audioonly.mp4 differ diff --git a/libraries/test_data/src/test/assets/transformerdumps/mp4/iibbibb_editlist_videoonly.mp4/transmuxed.dump b/libraries/test_data/src/test/assets/transformerdumps/mp4/iibbibb_editlist_videoonly.mp4/transmuxed.dump new file mode 100644 index 0000000000..b9935b7589 --- /dev/null +++ b/libraries/test_data/src/test/assets/transformerdumps/mp4/iibbibb_editlist_videoonly.mp4/transmuxed.dump @@ -0,0 +1,95 @@ +format video: + id = 1 + sampleMimeType = video/avc + codecs = avc1.F40016 + maxInputSize = 7838 + width = 704 + height = 576 + frameRate = 1.04 + colorInfo: + lumaBitdepth = 8 + chromaBitdepth = 8 + metadata = entries=[TSSE: description=null: values=[Lavf60.3.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] + initializationData: + data = length 30, hash 9DFD8D5 + data = length 9, hash FBADD682 +container metadata = entries=[TSSE: description=null: values=[Lavf60.3.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] +sample: + trackType = video + dataHashCode = 1491581480 + size = 7804 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackType = video + dataHashCode = -1689048121 + size = 7808 + isKeyFrame = true + presentationTimeUs = 2500000 +sample: + trackType = video + dataHashCode = 1018268785 + size = 1301 + isKeyFrame = false + presentationTimeUs = 1500000 +sample: + trackType = video + dataHashCode = -1625273408 + size = 1114 + isKeyFrame = false + presentationTimeUs = 500000 +sample: + trackType = video + dataHashCode = -374381878 + size = 7730 + isKeyFrame = true + presentationTimeUs = 5500000 +sample: + trackType = video + dataHashCode = -2004918573 + size = 1247 + isKeyFrame = false + presentationTimeUs = 4500000 +sample: + trackType = video + dataHashCode = 1940057858 + size = 1110 + isKeyFrame = false + presentationTimeUs = 3500000 +sample: + trackType = video + dataHashCode = 472148756 + size = 7595 + isKeyFrame = true + presentationTimeUs = 8500000 +sample: + trackType = video + dataHashCode = 911200371 + size = 1273 + isKeyFrame = false + presentationTimeUs = 7500000 +sample: + trackType = video + dataHashCode = -954114383 + size = 1130 + isKeyFrame = false + presentationTimeUs = 6500000 +sample: + trackType = video + dataHashCode = 77841273 + size = 6734 + isKeyFrame = true + presentationTimeUs = 11500000 +sample: + trackType = video + dataHashCode = 1932832421 + size = 1437 + isKeyFrame = false + presentationTimeUs = 10500000 +sample: + trackType = video + dataHashCode = -2133964046 + size = 1186 + isKeyFrame = false + presentationTimeUs = 9500000 +released = true diff --git a/libraries/test_data/src/test/assets/transformerdumps/mp4/long_edit_list_audioonly.mp4/transmuxed.dump b/libraries/test_data/src/test/assets/transformerdumps/mp4/long_edit_list_audioonly.mp4/transmuxed.dump new file mode 100644 index 0000000000..07652a192e --- /dev/null +++ b/libraries/test_data/src/test/assets/transformerdumps/mp4/long_edit_list_audioonly.mp4/transmuxed.dump @@ -0,0 +1,423 @@ +format audio: + averageBitrate = 140021 + peakBitrate = 140781 + id = 1 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 476 + channelCount = 2 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: values=[Lavf60.3.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] + initializationData: + data = length 2, hash 5FF +container metadata = entries=[TSSE: description=null: values=[Lavf60.3.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] +sample: + trackType = audio + dataHashCode = -620111888 + size = 423 + isKeyFrame = true + presentationTimeUs = -16826 +sample: + trackType = audio + dataHashCode = -1530182437 + size = 411 + isKeyFrame = true + presentationTimeUs = 6394 +sample: + trackType = audio + dataHashCode = 1230616627 + size = 404 + isKeyFrame = true + presentationTimeUs = 29614 +sample: + trackType = audio + dataHashCode = -1245126751 + size = 403 + isKeyFrame = true + presentationTimeUs = 52834 +sample: + trackType = audio + dataHashCode = -1410779761 + size = 422 + isKeyFrame = true + presentationTimeUs = 76054 +sample: + trackType = audio + dataHashCode = -1047441584 + size = 418 + isKeyFrame = true + presentationTimeUs = 99274 +sample: + trackType = audio + dataHashCode = 1146337558 + size = 421 + isKeyFrame = true + presentationTimeUs = 122494 +sample: + trackType = audio + dataHashCode = 1933912787 + size = 400 + isKeyFrame = true + presentationTimeUs = 145714 +sample: + trackType = audio + dataHashCode = 252268411 + size = 400 + isKeyFrame = true + presentationTimeUs = 168934 +sample: + trackType = audio + dataHashCode = -329438270 + size = 410 + isKeyFrame = true + presentationTimeUs = 192154 +sample: + trackType = audio + dataHashCode = 165440818 + size = 391 + isKeyFrame = true + presentationTimeUs = 215374 +sample: + trackType = audio + dataHashCode = 1712500598 + size = 361 + isKeyFrame = true + presentationTimeUs = 238594 +sample: + trackType = audio + dataHashCode = 1264185277 + size = 391 + isKeyFrame = true + presentationTimeUs = 261814 +sample: + trackType = audio + dataHashCode = -1934387857 + size = 390 + isKeyFrame = true + presentationTimeUs = 285034 +sample: + trackType = audio + dataHashCode = -105475655 + size = 388 + isKeyFrame = true + presentationTimeUs = 308253 +sample: + trackType = audio + dataHashCode = 671697059 + size = 399 + isKeyFrame = true + presentationTimeUs = 331473 +sample: + trackType = audio + dataHashCode = -1835652942 + size = 390 + isKeyFrame = true + presentationTimeUs = 354693 +sample: + trackType = audio + dataHashCode = 1445535733 + size = 387 + isKeyFrame = true + presentationTimeUs = 377913 +sample: + trackType = audio + dataHashCode = 713025022 + size = 446 + isKeyFrame = true + presentationTimeUs = 401133 +sample: + trackType = audio + dataHashCode = 226962058 + size = 436 + isKeyFrame = true + presentationTimeUs = 424353 +sample: + trackType = audio + dataHashCode = -361162400 + size = 394 + isKeyFrame = true + presentationTimeUs = 447573 +sample: + trackType = audio + dataHashCode = -1091817776 + size = 417 + isKeyFrame = true + presentationTimeUs = 470793 +sample: + trackType = audio + dataHashCode = -33570145 + size = 442 + isKeyFrame = true + presentationTimeUs = 494013 +sample: + trackType = audio + dataHashCode = 791597935 + size = 416 + isKeyFrame = true + presentationTimeUs = 517233 +sample: + trackType = audio + dataHashCode = 486177154 + size = 396 + isKeyFrame = true + presentationTimeUs = 540453 +sample: + trackType = audio + dataHashCode = 697876210 + size = 395 + isKeyFrame = true + presentationTimeUs = 563673 +sample: + trackType = audio + dataHashCode = -1416713338 + size = 389 + isKeyFrame = true + presentationTimeUs = 586893 +sample: + trackType = audio + dataHashCode = 180955111 + size = 404 + isKeyFrame = true + presentationTimeUs = 610113 +sample: + trackType = audio + dataHashCode = 1614220208 + size = 418 + isKeyFrame = true + presentationTimeUs = 633333 +sample: + trackType = audio + dataHashCode = 1619215866 + size = 393 + isKeyFrame = true + presentationTimeUs = 656553 +sample: + trackType = audio + dataHashCode = -730291234 + size = 402 + isKeyFrame = true + presentationTimeUs = 679773 +sample: + trackType = audio + dataHashCode = -1734250280 + size = 404 + isKeyFrame = true + presentationTimeUs = 702993 +sample: + trackType = audio + dataHashCode = -24161892 + size = 397 + isKeyFrame = true + presentationTimeUs = 726213 +sample: + trackType = audio + dataHashCode = 1339131070 + size = 386 + isKeyFrame = true + presentationTimeUs = 749433 +sample: + trackType = audio + dataHashCode = 1996219279 + size = 377 + isKeyFrame = true + presentationTimeUs = 772653 +sample: + trackType = audio + dataHashCode = -1832486999 + size = 409 + isKeyFrame = true + presentationTimeUs = 795873 +sample: + trackType = audio + dataHashCode = 1120922082 + size = 402 + isKeyFrame = true + presentationTimeUs = 819092 +sample: + trackType = audio + dataHashCode = -1514208718 + size = 390 + isKeyFrame = true + presentationTimeUs = 842312 +sample: + trackType = audio + dataHashCode = 1442267846 + size = 388 + isKeyFrame = true + presentationTimeUs = 865532 +sample: + trackType = audio + dataHashCode = -2064065315 + size = 377 + isKeyFrame = true + presentationTimeUs = 888752 +sample: + trackType = audio + dataHashCode = -502416917 + size = 391 + isKeyFrame = true + presentationTimeUs = 911972 +sample: + trackType = audio + dataHashCode = 730491655 + size = 398 + isKeyFrame = true + presentationTimeUs = 935192 +sample: + trackType = audio + dataHashCode = -1910342128 + size = 381 + isKeyFrame = true + presentationTimeUs = 958412 +sample: + trackType = audio + dataHashCode = 475981018 + size = 393 + isKeyFrame = true + presentationTimeUs = 981632 +sample: + trackType = audio + dataHashCode = -746607724 + size = 393 + isKeyFrame = true + presentationTimeUs = 1004852 +sample: + trackType = audio + dataHashCode = 1641658609 + size = 380 + isKeyFrame = true + presentationTimeUs = 1028072 +sample: + trackType = audio + dataHashCode = -1148163632 + size = 395 + isKeyFrame = true + presentationTimeUs = 1051292 +sample: + trackType = audio + dataHashCode = 665110699 + size = 379 + isKeyFrame = true + presentationTimeUs = 1074512 +sample: + trackType = audio + dataHashCode = 798207150 + size = 403 + isKeyFrame = true + presentationTimeUs = 1097732 +sample: + trackType = audio + dataHashCode = 1359581525 + size = 415 + isKeyFrame = true + presentationTimeUs = 1120952 +sample: + trackType = audio + dataHashCode = -335439207 + size = 400 + isKeyFrame = true + presentationTimeUs = 1144172 +sample: + trackType = audio + dataHashCode = -198311225 + size = 401 + isKeyFrame = true + presentationTimeUs = 1167392 +sample: + trackType = audio + dataHashCode = -924672246 + size = 400 + isKeyFrame = true + presentationTimeUs = 1190612 +sample: + trackType = audio + dataHashCode = -1282928372 + size = 408 + isKeyFrame = true + presentationTimeUs = 1213832 +sample: + trackType = audio + dataHashCode = -50597927 + size = 406 + isKeyFrame = true + presentationTimeUs = 1237052 +sample: + trackType = audio + dataHashCode = -1671375815 + size = 411 + isKeyFrame = true + presentationTimeUs = 1260272 +sample: + trackType = audio + dataHashCode = -1897083943 + size = 414 + isKeyFrame = true + presentationTimeUs = 1283492 +sample: + trackType = audio + dataHashCode = -1693247556 + size = 393 + isKeyFrame = true + presentationTimeUs = 1306712 +sample: + trackType = audio + dataHashCode = 1287376831 + size = 405 + isKeyFrame = true + presentationTimeUs = 1329931 +sample: + trackType = audio + dataHashCode = -1463566839 + size = 412 + isKeyFrame = true + presentationTimeUs = 1353151 +sample: + trackType = audio + dataHashCode = -841250803 + size = 409 + isKeyFrame = true + presentationTimeUs = 1376371 +sample: + trackType = audio + dataHashCode = 167279197 + size = 423 + isKeyFrame = true + presentationTimeUs = 1399591 +sample: + trackType = audio + dataHashCode = 1819296695 + size = 399 + isKeyFrame = true + presentationTimeUs = 1422811 +sample: + trackType = audio + dataHashCode = 696153948 + size = 400 + isKeyFrame = true + presentationTimeUs = 1446031 +sample: + trackType = audio + dataHashCode = 1462953378 + size = 397 + isKeyFrame = true + presentationTimeUs = 1469251 +sample: + trackType = audio + dataHashCode = 310189811 + size = 398 + isKeyFrame = true + presentationTimeUs = 1492471 +sample: + trackType = audio + dataHashCode = -1082736404 + size = 424 + isKeyFrame = true + presentationTimeUs = 1515691 +sample: + trackType = audio + dataHashCode = 1980862648 + size = 417 + isKeyFrame = true + presentationTimeUs = 1538911 +released = true 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 971efef128..0600099c10 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -68,6 +68,11 @@ import androidx.media3.effect.RgbFilter; import androidx.media3.effect.ScaleAndRotateTransformation; import androidx.media3.effect.TimestampWrapper; import androidx.media3.exoplayer.audio.TeeAudioProcessor; +import androidx.media3.extractor.mp4.Mp4Extractor; +import androidx.media3.extractor.text.DefaultSubtitleParserFactory; +import androidx.media3.test.utils.FakeExtractorOutput; +import androidx.media3.test.utils.FakeTrackOutput; +import androidx.media3.test.utils.TestUtil; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; @@ -923,6 +928,118 @@ public class TransformerEndToEndTest { assertThat(result.exportResult.channelCount).isEqualTo(2); } + @Test + public void transmux_audioWithEditList_preservesDuration() throws Exception { + String testId = "transmux_audioWithEditList_preservesDuration"; + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = new Transformer.Builder(context).build(); + MediaItem mediaItem = + MediaItem.fromUri(Uri.parse("asset:///media/mp4/long_edit_list_audioonly.mp4")); + + ExportTestResult exportTestResult = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(testId, mediaItem); + + Mp4Extractor mp4Extractor = new Mp4Extractor(new DefaultSubtitleParserFactory()); + FakeExtractorOutput fakeExtractorOutput = + TestUtil.extractAllSamplesFromFilePath(mp4Extractor, exportTestResult.filePath); + // TODO: b/324842222 - Mp4Extractor reports incorrect duration, without considering edit lists. + assertThat(fakeExtractorOutput.seekMap.getDurationUs()).isEqualTo(1_579_000); + assertThat(fakeExtractorOutput.numberOfTracks).isEqualTo(1); + FakeTrackOutput audioTrack = fakeExtractorOutput.trackOutputs.get(0); + int expectedSampleCount = 68; + audioTrack.assertSampleCount(expectedSampleCount); + if (Util.SDK_INT >= 30) { + // TODO: b/324842222 - Mp4Extractor doesn't interpret Transformer's generated output as + // "gapless" audio. The generated file should have encoderDelay = 742 and first + // sample PTS of 0. + assertThat(audioTrack.lastFormat.encoderDelay).isEqualTo(0); + assertThat(audioTrack.getSampleTimeUs(/* index= */ 0)).isEqualTo(-16_826); + assertThat(audioTrack.getSampleTimeUs(/* index= */ expectedSampleCount - 1)) + .isEqualTo(1_538_911); + } else { + // Edit lists are not supported b/142580952 : sample times start from zero, + // and output duration will be longer than input duration by encoder delay. + assertThat(audioTrack.lastFormat.encoderDelay).isEqualTo(0); + assertThat(audioTrack.getSampleTimeUs(/* index= */ 0)).isEqualTo(0); + assertThat(audioTrack.getSampleTimeUs(/* index= */ expectedSampleCount - 1)) + .isEqualTo(1_555_736); + } + } + + @Test + public void transmux_audioWithEditListUsingInAppMuxer_preservesDuration() throws Exception { + String testId = "transmux_AudioWithEditListUsingInAppMuxer_preservesDuration"; + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder(context) + .setMuxerFactory(new InAppMuxer.Factory.Builder().build()) + .build(); + MediaItem mediaItem = + MediaItem.fromUri(Uri.parse("asset:///media/mp4/long_edit_list_audioonly.mp4")); + + ExportTestResult exportTestResult = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(testId, mediaItem); + + Mp4Extractor mp4Extractor = new Mp4Extractor(new DefaultSubtitleParserFactory()); + FakeExtractorOutput fakeExtractorOutput = + TestUtil.extractAllSamplesFromFilePath(mp4Extractor, exportTestResult.filePath); + // TODO: b/324903070 - The generated output file has incorrect duration. + assertThat(fakeExtractorOutput.seekMap.getDurationUs()).isEqualTo(1_555_700); + assertThat(fakeExtractorOutput.numberOfTracks).isEqualTo(1); + FakeTrackOutput audioTrack = fakeExtractorOutput.trackOutputs.get(0); + int expectedSampleCount = 68; + audioTrack.assertSampleCount(expectedSampleCount); + // TODO: b/324903070 - InAppMuxer doesn't write edit lists to support gapless audio muxing. + // Output incorrectly starts at encoderDelay 0, PTS 0 + assertThat(audioTrack.lastFormat.encoderDelay).isEqualTo(0); + assertThat(audioTrack.getSampleTimeUs(/* index= */ 0)).isEqualTo(0); + // TODO: b/270583563 - InAppMuxer always uses 1 / 48_000 timebase for audio. + // The audio file in this test is 44_100 Hz, with timebase for audio of 1 / 44_100 and + // each sample duration is exactly 1024 / 44_100, with no rounding errors. + // Since InAppMuxer uses a different timebase for audio, some rounding errors are introduced + // and MP4 sample durations are off. + // TODO: b/324903070 - expectedLastSampleTimeUs & expectedDurationUs are incorrect. + // Last sample time cannot be greater than total duration. + assertThat(audioTrack.getSampleTimeUs(/* index= */ expectedSampleCount - 1)) + .isEqualTo(1_555_708); + } + + @Test + public void transmux_videoWithEditList_trimsFirstIDRFrameDuration() throws Exception { + String testId = "transmux_videoWithEditList_trimsFirstIDRFrameDuration"; + Context context = ApplicationProvider.getApplicationContext(); + assumeTrue( + "MediaMuxer doesn't support B frames reliably on older SDK versions", Util.SDK_INT >= 29); + Transformer transformer = new Transformer.Builder(context).build(); + MediaItem mediaItem = + MediaItem.fromUri(Uri.parse("asset:///media/mp4/iibbibb_editlist_videoonly.mp4")); + + ExportTestResult exportTestResult = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(testId, mediaItem); + + Mp4Extractor mp4Extractor = new Mp4Extractor(new DefaultSubtitleParserFactory()); + FakeExtractorOutput fakeExtractorOutput = + TestUtil.extractAllSamplesFromFilePath(mp4Extractor, exportTestResult.filePath); + assertThat(fakeExtractorOutput.numberOfTracks).isEqualTo(1); + + // TODO: b/324842222 - Duration isn't written correctly when transmuxing, and differs + // between SDK versions. Do not assert for duration yet. + FakeTrackOutput videoTrack = fakeExtractorOutput.trackOutputs.get(0); + int expectedSampleCount = 13; + videoTrack.assertSampleCount(expectedSampleCount); + assertThat(videoTrack.getSampleTimeUs(/* index= */ 0)).isEqualTo(0); + int sampleIndexWithLargestSampleTime = 10; + assertThat(videoTrack.getSampleTimeUs(sampleIndexWithLargestSampleTime)).isEqualTo(11_500_000); + assertThat(videoTrack.getSampleTimeUs(/* index= */ expectedSampleCount - 1)) + .isEqualTo(9_500_000); + } + private static AudioProcessor createSonic(float pitch) { SonicAudioProcessor sonic = new SonicAudioProcessor(); sonic.setPitch(pitch); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java index 638dbc74e9..fd7665789c 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java @@ -91,6 +91,7 @@ import java.nio.ByteBuffer; private final long videoDurationUs; private final MediaCodec.BufferInfo bufferInfo; private final SparseLongArray trackIndexToLastPresentationTimeUs; + private final SparseLongArray trackIndexToPresentationTimeOffsetUs; private int videoTrackIndex; @@ -103,6 +104,7 @@ import java.nio.ByteBuffer; this.videoDurationUs = Util.msToUs(videoDurationMs); bufferInfo = new MediaCodec.BufferInfo(); trackIndexToLastPresentationTimeUs = new SparseLongArray(); + trackIndexToPresentationTimeOffsetUs = new SparseLongArray(); videoTrackIndex = C.INDEX_UNSET; } @@ -153,6 +155,9 @@ import java.nio.ByteBuffer; if (!isStarted) { isStarted = true; + if (Util.SDK_INT < 30 && presentationTimeUs < 0) { + trackIndexToPresentationTimeOffsetUs.put(trackIndex, -presentationTimeUs); + } try { mediaMuxer.start(); } catch (RuntimeException e) { @@ -163,6 +168,9 @@ import java.nio.ByteBuffer; int offset = data.position(); int size = data.limit() - offset; + long presentationTimeOffsetUs = trackIndexToPresentationTimeOffsetUs.get(trackIndex); + presentationTimeUs += presentationTimeOffsetUs; + bufferInfo.set(offset, size, presentationTimeUs, TransformerUtil.getMediaCodecFlags(flags)); long lastSamplePresentationTimeUs = trackIndexToLastPresentationTimeUs.get(trackIndex); // writeSampleData blocks on old API versions, so check here to avoid calling the method. @@ -174,6 +182,15 @@ import java.nio.ByteBuffer; + lastSamplePresentationTimeUs + ") unsupported on this API version"); trackIndexToLastPresentationTimeUs.put(trackIndex, presentationTimeUs); + + checkState( + presentationTimeOffsetUs == 0 || presentationTimeUs >= lastSamplePresentationTimeUs, + "Samples not in presentation order (" + + presentationTimeUs + + " < " + + lastSamplePresentationTimeUs + + ") unsupported when using negative PTS workaround"); + try { mediaMuxer.writeSampleData(trackIndex, data, bufferInfo); } catch (RuntimeException e) { 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 169e12637d..171d8fb478 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java @@ -19,15 +19,18 @@ 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.DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS; 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; import static androidx.media3.transformer.TestUtil.FILE_AUDIO_AMR_NB; import static androidx.media3.transformer.TestUtil.FILE_AUDIO_AMR_WB; +import static androidx.media3.transformer.TestUtil.FILE_AUDIO_ELST_SKIP_500MS; import static androidx.media3.transformer.TestUtil.FILE_AUDIO_RAW; import static androidx.media3.transformer.TestUtil.FILE_AUDIO_VIDEO; import static androidx.media3.transformer.TestUtil.FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S; import static androidx.media3.transformer.TestUtil.FILE_UNKNOWN_DURATION; +import static androidx.media3.transformer.TestUtil.FILE_VIDEO_ELST_TRIM_IDR_DURATION; import static androidx.media3.transformer.TestUtil.FILE_VIDEO_ONLY; import static androidx.media3.transformer.TestUtil.FILE_WITH_SEF_SLOW_MOTION; import static androidx.media3.transformer.TestUtil.FILE_WITH_SUBTITLES; @@ -66,6 +69,7 @@ import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; import androidx.media3.common.audio.SonicAudioProcessor; +import androidx.media3.effect.Contrast; import androidx.media3.effect.Presentation; import androidx.media3.effect.ScaleAndRotateTransformation; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; @@ -101,6 +105,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowMediaCodec; /** @@ -1381,6 +1386,79 @@ public final class MediaItemExportTest { assertThat(illegalStateException.get()).isNotNull(); } + @Test + @Config(minSdk = 30) + // This test requires Android SDK >= 30 for MediaMuxer negative PTS support. + public void transmux_audioWithEditList_api30_correctDuration() throws Exception { + Transformer transformer = + createTransformerBuilder(muxerFactory, /* enableFallback= */ false).build(); + MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_ELST_SKIP_500MS); + + transformer.start(mediaItem, outputDir.newFile().getPath()); + ExportResult result = TransformerTestRunner.runLooper(transformer); + + // TODO: b/324245196 - Update this test when bugs are fixed. + // Duration is actually 68267 / 44100 = 1548ms. + // Last frame PTS is 67866 / 44100 = 1.53891 which rounds down to 1538ms. + assertThat(result.durationMs).isEqualTo(1538); + // TODO: b/325020444 - Update this test when bugs are fixed. + // Dump incorrectly includes the last clipped audio sample from input file. + DumpFileAsserts.assertOutput( + context, + muxerFactory.getCreatedMuxer(), + getDumpFileName( + /* originalFileName= */ FILE_AUDIO_ELST_SKIP_500MS, + /* modifications...= */ "transmuxed")); + } + + @Test + @Config(minSdk = 21, maxSdk = 29) + // This test requires Android SDK < 30 with no MediaMuxer negative PTS support. + public void transmux_audioWithEditList_api29_frameworkMuxerDoesNotThrow() throws Exception { + // Do not use CapturingMuxer.Factory(), as this test checks for a workaround in + // FrameworkMuxer. + Transformer transformer = + createTransformerBuilder( + new FrameworkMuxer.Factory(DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS, C.TIME_UNSET), + /* enableFallback= */ false) + .build(); + MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_ELST_SKIP_500MS); + + transformer.start(mediaItem, outputDir.newFile().getPath()); + ExportResult result = TransformerTestRunner.runLooper(transformer); + + // TODO: b/324842222 - Update this test when bugs are fixed. + // The result.durationMs is incorrect in this test because + // FrameworkMuxer workaround doesn't propagate changed timestamps to MuxerWrapper. + assertThat(result.durationMs).isEqualTo(1538); + assertThat(result.exportException).isNull(); + } + + @Test + @Config(minSdk = 25) + // This test requires Android SDK < 30 for lack of MediaMuxer negative PTS support + // and SDK >= 25 for B-frame support. + public void transmux_trimsFirstIDRDuration() throws Exception { + Transformer transformer = + createTransformerBuilder(muxerFactory, /* enableFallback= */ false).build(); + MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ELST_TRIM_IDR_DURATION); + + transformer.start(mediaItem, outputDir.newFile().getPath()); + ExportResult result = TransformerTestRunner.runLooper(transformer); + + // TODO: b/324245196 - Update this test when bugs are fixed. + // Duration is actually 12_500. Last frame PTS is 11_500. + assertThat(result.durationMs).isEqualTo(11_500); + int inputFrameCount = 13; + assertThat(result.videoFrameCount).isEqualTo(inputFrameCount); + DumpFileAsserts.assertOutput( + context, + muxerFactory.getCreatedMuxer(), + getDumpFileName( + /* originalFileName= */ FILE_VIDEO_ELST_TRIM_IDR_DURATION, + /* modifications...= */ "transmuxed")); + } + private static final class SlowExtractorsFactory implements ExtractorsFactory { private final long delayBetweenReadsMs; diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TestUtil.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TestUtil.java index 751baad79e..7907629196 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TestUtil.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TestUtil.java @@ -53,6 +53,9 @@ public final class TestUtil { public static final String FILE_AUDIO_AMR_NB = "amr/sample_nb.amr"; public static final String FILE_AUDIO_AC3_UNSUPPORTED_BY_MUXER = "mp4/sample_ac3.mp4"; public static final String FILE_UNKNOWN_DURATION = "mp4/sample_fragmented.mp4"; + public static final String FILE_AUDIO_ELST_SKIP_500MS = "mp4/long_edit_list_audioonly.mp4"; + public static final String FILE_VIDEO_ELST_TRIM_IDR_DURATION = + "mp4/iibbibb_editlist_videoonly.mp4"; private static final String DUMP_FILE_OUTPUT_DIRECTORY = "transformerdumps"; private static final String DUMP_FILE_EXTENSION = "dump"; @@ -60,7 +63,7 @@ public final class TestUtil { private TestUtil() {} public static Transformer.Builder createTransformerBuilder( - CapturingMuxer.Factory muxerFactory, boolean enableFallback) { + Muxer.Factory muxerFactory, boolean enableFallback) { Context context = ApplicationProvider.getApplicationContext(); return new Transformer.Builder(context) .setClock(new FakeClock(/* isAutoAdvancing= */ true))