diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java b/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java index a654518e34..ed6a69cd3f 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java @@ -162,7 +162,8 @@ import org.checkerframework.checker.nullness.qual.PolyNull; } String languageCode = bcp47LanguageTagToIso3(format.language); - // Generate the sample durations to calculate the total duration for tkhd box. + // Generate the sample durations to calculate the total duration for tkhd, elst and mvhd + // boxes. List sampleDurationsVu = convertPresentationTimestampsToDurationsVu( track.writtenSamples, @@ -178,6 +179,8 @@ import org.checkerframework.checker.nullness.qual.PolyNull; long firstInputPtsUs = track.writtenSamples.isEmpty() ? 0 : track.writtenSamples.get(0).presentationTimeUs; long trackDurationUs = usFromVu(trackDurationInTrackUnitsVu, track.videoUnitTimebase()); + long presentationTrackDurationUs = + firstInputPtsUs < 0 ? trackDurationUs - abs(firstInputPtsUs) : trackDurationUs; @C.TrackType int trackType = MimeTypes.getTrackType(format.sampleMimeType); ByteBuffer stts = stts(sampleDurationsVu); @@ -232,7 +235,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull; trak( tkhd( nextTrackId, - trackDurationUs, + presentationTrackDurationUs, creationTimestampSeconds, modificationTimestampSeconds, metadataCollector.orientationData.orientation, @@ -240,7 +243,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull; edts( firstInputPtsUs, minInputPtsUs, - trackDurationUs, + presentationTrackDurationUs, MVHD_TIMEBASE, track.videoUnitTimebase()), mdia( @@ -254,7 +257,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull; minf(mhdBox, dinf(dref(localUrl())), stblBox))); trakBoxes.add(trakBox); - videoDurationUs = max(videoDurationUs, trackDurationUs); + videoDurationUs = max(videoDurationUs, presentationTrackDurationUs); trexBoxes.add(trex(nextTrackId)); nextTrackId++; } @@ -853,8 +856,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull; elstContent.putInt(1); // Entry count elstContent.put( elstEntry( - /* editDurationVu= */ vuFromUs( - trackDurationUs - abs(firstSamplePtsUs), mvhdTimescale), + /* editDurationVu= */ vuFromUs(trackDurationUs, mvhdTimescale), /* mediaTimeVu= */ vuFromUs(abs(firstSamplePtsUs), trackTimescale), /* mediaRateInt= */ 1, /* mediaRateFraction= */ 0)); diff --git a/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java b/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java index b76f6c7ec0..ee1ef3e8f9 100644 --- a/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java +++ b/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java @@ -593,6 +593,40 @@ public class BoxesTest { assertThat(durationsVu).containsExactly(100, 100, 800, 100, 0); } + @Test + public void + convertPresentationTimestampsToDurationsVu_withNegativeSampleTimestampsAndZero_returnsExpectedDurations() { + List sampleBufferInfos = + createBufferInfoListWithSamplePresentationTimestamps( + -1_000L, 0L, 10_000L, 1_000L, 2_000L, 11_000L); + + List durationsVu = + Boxes.convertPresentationTimestampsToDurationsVu( + sampleBufferInfos, + VU_TIMEBASE, + LAST_SAMPLE_DURATION_BEHAVIOR_SET_TO_ZERO, + C.TIME_UNSET); + + assertThat(durationsVu).containsExactly(100, 100, 100, 800, 100, 0); + } + + @Test + public void + convertPresentationTimestampsToDurationsVu_withNegativeSampleTimestamps_returnsExpectedDurations() { + List sampleBufferInfos = + createBufferInfoListWithSamplePresentationTimestamps( + -1_000L, 10_000L, 1_000L, 2_000L, 11_000L); + + List durationsVu = + Boxes.convertPresentationTimestampsToDurationsVu( + sampleBufferInfos, + VU_TIMEBASE, + LAST_SAMPLE_DURATION_BEHAVIOR_SET_TO_ZERO, + C.TIME_UNSET); + + assertThat(durationsVu).containsExactly(200, 100, 800, 100, 0); + } + @Test public void convertPresentationTimestampsToDurationsVu_withLastSampleDurationBehaviorUsingEndOfStreamFlag_returnsExpectedDurations() { diff --git a/libraries/test_data/src/test/assets/muxerdumps/mp4_with_negative_tracks_offset.mp4.dump b/libraries/test_data/src/test/assets/muxerdumps/mp4_with_negative_tracks_offset.mp4.dump index bb96e0b704..d1710a02a3 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/mp4_with_negative_tracks_offset.mp4.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/mp4_with_negative_tracks_offset.mp4.dump @@ -1,15 +1,15 @@ seekMap: isSeekable = true - duration = 600 + duration = 500 getPosition(0) = [[timeUs=-100, position=400052], [timeUs=100, position=400108]] getPosition(1) = [[timeUs=-100, position=400052], [timeUs=100, position=400108]] - getPosition(300) = [[timeUs=300, position=400164]] - getPosition(600) = [[timeUs=300, position=400164]] + getPosition(250) = [[timeUs=100, position=400108], [timeUs=300, position=400164]] + getPosition(500) = [[timeUs=300, position=400164]] numberOfTracks = 1 track 0: total output bytes = 168 sample count = 3 - track duration = 600 + track duration = 500 format 0: averageBitrate = 2240000 id = 1 diff --git a/libraries/test_data/src/test/assets/muxerdumps/negative_start_time_edts_box.dump b/libraries/test_data/src/test/assets/muxerdumps/negative_start_time_edts_box.dump index 89afcc0cc5..59e74029d9 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/negative_start_time_edts_box.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/negative_start_time_edts_box.dump @@ -1,3 +1,3 @@ edts (44 bytes): elst (36 bytes): - Data = length 28, hash 68F2D7C9 + Data = length 28, hash 75FF2BCC diff --git a/libraries/test_data/src/test/assets/muxerdumps/sample_edit_list.mp4.dump b/libraries/test_data/src/test/assets/muxerdumps/sample_edit_list.mp4.dump index 2b5c39704f..79619edae2 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/sample_edit_list.mp4.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/sample_edit_list.mp4.dump @@ -1,15 +1,15 @@ seekMap: isSeekable = true - duration = 3135000 + duration = 2680000 getPosition(0) = [[timeUs=-455000, position=400052], [timeUs=611666, position=411095]] getPosition(1) = [[timeUs=-455000, position=400052], [timeUs=611666, position=411095]] - getPosition(1567500) = [[timeUs=611666, position=411095], [timeUs=1680000, position=429829]] - getPosition(3135000) = [[timeUs=1680000, position=429829]] + getPosition(1340000) = [[timeUs=611666, position=411095], [timeUs=1680000, position=429829]] + getPosition(2680000) = [[timeUs=1680000, position=429829]] numberOfTracks = 3 track 0: total output bytes = 3208515 sample count = 85 - track duration = 3135000 + track duration = 2680000 format 0: averageBitrate = 8187598 id = 1 @@ -376,7 +376,7 @@ track 0: track 1: total output bytes = 3208515 sample count = 85 - track duration = 3135000 + track duration = 2680000 format 0: averageBitrate = 8187598 id = 2 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 876d63fb68..4eec3e4456 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -2153,8 +2153,7 @@ public class TransformerEndToEndTest { 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_579_600); + assertThat(fakeExtractorOutput.seekMap.getDurationUs()).isEqualTo(1_562_800); assertThat(fakeExtractorOutput.numberOfTracks).isEqualTo(1); FakeTrackOutput audioTrack = fakeExtractorOutput.trackOutputs.get(0); int expectedSampleCount = 68;