Update Boxes to support writing negative timestamps to edit list

Previously when there were negative timestamps, the tkhd duration was incorrectly equal to the full track duration rather than the presentation duration of the edit list. From the [docs](https://developer.apple.com/documentation/quicktime-file-format/track_header_atom/duration) - "The value of this field is equal to the sum of the durations of all of the track’s edits".

PiperOrigin-RevId: 752655137
This commit is contained in:
Googler 2025-04-29 02:57:34 -07:00 committed by Copybara-Service
parent 48832cbbc4
commit cfa13e9616
6 changed files with 53 additions and 18 deletions

View File

@ -162,7 +162,8 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
} }
String languageCode = bcp47LanguageTagToIso3(format.language); 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<Integer> sampleDurationsVu = List<Integer> sampleDurationsVu =
convertPresentationTimestampsToDurationsVu( convertPresentationTimestampsToDurationsVu(
track.writtenSamples, track.writtenSamples,
@ -178,6 +179,8 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
long firstInputPtsUs = long firstInputPtsUs =
track.writtenSamples.isEmpty() ? 0 : track.writtenSamples.get(0).presentationTimeUs; track.writtenSamples.isEmpty() ? 0 : track.writtenSamples.get(0).presentationTimeUs;
long trackDurationUs = usFromVu(trackDurationInTrackUnitsVu, track.videoUnitTimebase()); long trackDurationUs = usFromVu(trackDurationInTrackUnitsVu, track.videoUnitTimebase());
long presentationTrackDurationUs =
firstInputPtsUs < 0 ? trackDurationUs - abs(firstInputPtsUs) : trackDurationUs;
@C.TrackType int trackType = MimeTypes.getTrackType(format.sampleMimeType); @C.TrackType int trackType = MimeTypes.getTrackType(format.sampleMimeType);
ByteBuffer stts = stts(sampleDurationsVu); ByteBuffer stts = stts(sampleDurationsVu);
@ -232,7 +235,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
trak( trak(
tkhd( tkhd(
nextTrackId, nextTrackId,
trackDurationUs, presentationTrackDurationUs,
creationTimestampSeconds, creationTimestampSeconds,
modificationTimestampSeconds, modificationTimestampSeconds,
metadataCollector.orientationData.orientation, metadataCollector.orientationData.orientation,
@ -240,7 +243,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
edts( edts(
firstInputPtsUs, firstInputPtsUs,
minInputPtsUs, minInputPtsUs,
trackDurationUs, presentationTrackDurationUs,
MVHD_TIMEBASE, MVHD_TIMEBASE,
track.videoUnitTimebase()), track.videoUnitTimebase()),
mdia( mdia(
@ -254,7 +257,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
minf(mhdBox, dinf(dref(localUrl())), stblBox))); minf(mhdBox, dinf(dref(localUrl())), stblBox)));
trakBoxes.add(trakBox); trakBoxes.add(trakBox);
videoDurationUs = max(videoDurationUs, trackDurationUs); videoDurationUs = max(videoDurationUs, presentationTrackDurationUs);
trexBoxes.add(trex(nextTrackId)); trexBoxes.add(trex(nextTrackId));
nextTrackId++; nextTrackId++;
} }
@ -853,8 +856,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
elstContent.putInt(1); // Entry count elstContent.putInt(1); // Entry count
elstContent.put( elstContent.put(
elstEntry( elstEntry(
/* editDurationVu= */ vuFromUs( /* editDurationVu= */ vuFromUs(trackDurationUs, mvhdTimescale),
trackDurationUs - abs(firstSamplePtsUs), mvhdTimescale),
/* mediaTimeVu= */ vuFromUs(abs(firstSamplePtsUs), trackTimescale), /* mediaTimeVu= */ vuFromUs(abs(firstSamplePtsUs), trackTimescale),
/* mediaRateInt= */ 1, /* mediaRateInt= */ 1,
/* mediaRateFraction= */ 0)); /* mediaRateFraction= */ 0));

View File

@ -593,6 +593,40 @@ public class BoxesTest {
assertThat(durationsVu).containsExactly(100, 100, 800, 100, 0); assertThat(durationsVu).containsExactly(100, 100, 800, 100, 0);
} }
@Test
public void
convertPresentationTimestampsToDurationsVu_withNegativeSampleTimestampsAndZero_returnsExpectedDurations() {
List<BufferInfo> sampleBufferInfos =
createBufferInfoListWithSamplePresentationTimestamps(
-1_000L, 0L, 10_000L, 1_000L, 2_000L, 11_000L);
List<Integer> 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<BufferInfo> sampleBufferInfos =
createBufferInfoListWithSamplePresentationTimestamps(
-1_000L, 10_000L, 1_000L, 2_000L, 11_000L);
List<Integer> 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 @Test
public void public void
convertPresentationTimestampsToDurationsVu_withLastSampleDurationBehaviorUsingEndOfStreamFlag_returnsExpectedDurations() { convertPresentationTimestampsToDurationsVu_withLastSampleDurationBehaviorUsingEndOfStreamFlag_returnsExpectedDurations() {

View File

@ -1,15 +1,15 @@
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 600 duration = 500
getPosition(0) = [[timeUs=-100, position=400052], [timeUs=100, position=400108]] getPosition(0) = [[timeUs=-100, position=400052], [timeUs=100, position=400108]]
getPosition(1) = [[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(250) = [[timeUs=100, position=400108], [timeUs=300, position=400164]]
getPosition(600) = [[timeUs=300, position=400164]] getPosition(500) = [[timeUs=300, position=400164]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
total output bytes = 168 total output bytes = 168
sample count = 3 sample count = 3
track duration = 600 track duration = 500
format 0: format 0:
averageBitrate = 2240000 averageBitrate = 2240000
id = 1 id = 1

View File

@ -1,3 +1,3 @@
edts (44 bytes): edts (44 bytes):
elst (36 bytes): elst (36 bytes):
Data = length 28, hash 68F2D7C9 Data = length 28, hash 75FF2BCC

View File

@ -1,15 +1,15 @@
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 3135000 duration = 2680000
getPosition(0) = [[timeUs=-455000, position=400052], [timeUs=611666, position=411095]] getPosition(0) = [[timeUs=-455000, position=400052], [timeUs=611666, position=411095]]
getPosition(1) = [[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(1340000) = [[timeUs=611666, position=411095], [timeUs=1680000, position=429829]]
getPosition(3135000) = [[timeUs=1680000, position=429829]] getPosition(2680000) = [[timeUs=1680000, position=429829]]
numberOfTracks = 3 numberOfTracks = 3
track 0: track 0:
total output bytes = 3208515 total output bytes = 3208515
sample count = 85 sample count = 85
track duration = 3135000 track duration = 2680000
format 0: format 0:
averageBitrate = 8187598 averageBitrate = 8187598
id = 1 id = 1
@ -376,7 +376,7 @@ track 0:
track 1: track 1:
total output bytes = 3208515 total output bytes = 3208515
sample count = 85 sample count = 85
track duration = 3135000 track duration = 2680000
format 0: format 0:
averageBitrate = 8187598 averageBitrate = 8187598
id = 2 id = 2

View File

@ -2153,8 +2153,7 @@ public class TransformerEndToEndTest {
Mp4Extractor mp4Extractor = new Mp4Extractor(new DefaultSubtitleParserFactory()); Mp4Extractor mp4Extractor = new Mp4Extractor(new DefaultSubtitleParserFactory());
FakeExtractorOutput fakeExtractorOutput = FakeExtractorOutput fakeExtractorOutput =
TestUtil.extractAllSamplesFromFilePath(mp4Extractor, exportTestResult.filePath); TestUtil.extractAllSamplesFromFilePath(mp4Extractor, exportTestResult.filePath);
// TODO: b/324903070 - The generated output file has incorrect duration. assertThat(fakeExtractorOutput.seekMap.getDurationUs()).isEqualTo(1_562_800);
assertThat(fakeExtractorOutput.seekMap.getDurationUs()).isEqualTo(1_579_600);
assertThat(fakeExtractorOutput.numberOfTracks).isEqualTo(1); assertThat(fakeExtractorOutput.numberOfTracks).isEqualTo(1);
FakeTrackOutput audioTrack = fakeExtractorOutput.trackOutputs.get(0); FakeTrackOutput audioTrack = fakeExtractorOutput.trackOutputs.get(0);
int expectedSampleCount = 68; int expectedSampleCount = 68;