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);
// 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 =
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));

View File

@ -593,6 +593,40 @@ public class BoxesTest {
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
public void
convertPresentationTimestampsToDurationsVu_withLastSampleDurationBehaviorUsingEndOfStreamFlag_returnsExpectedDurations() {

View File

@ -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

View File

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

View File

@ -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

View File

@ -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;