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 9d76711142..8c6edc3acf 100644
--- a/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java
+++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java
@@ -23,6 +23,7 @@ import static androidx.media3.muxer.ColorUtils.MEDIAFORMAT_TRANSFER_TO_MP4_TRANS
import static androidx.media3.muxer.Mp4Utils.MVHD_TIMEBASE;
import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.ColorInfo;
@@ -345,7 +346,7 @@ import java.util.Locale;
}
String locationString =
- String.format(Locale.US, "%+.4f%+.4f/", location.latitude, location.longitude);
+ Util.formatInvariant("%+.4f%+.4f/", location.latitude, location.longitude);
ByteBuffer xyzBoxContents = ByteBuffer.allocate(locationString.length() + 2 + 2);
xyzBoxContents.putShort((short) (xyzBoxContents.capacity() - 4));
@@ -580,53 +581,54 @@ import java.util.Locale;
}
/**
- * Converts sample presentation times (in microseconds) to sample durations (in timebase units)
- * that will go into the stts box.
+ * Converts sample presentation times (in microseconds) to sample durations (in timebase units).
*
- *
ISO/IEC 14496-12: 8.6.1.3.1 recommends each track starts at 0. Therefore, the first sample
- * presentation timestamp is set to 0 and the duration of that sample may be larger as a result.
+ *
All the tracks must start from the same time. If all the tracks do not start from the same
+ * time, then the caller must pass the minimum presentation timestamp across all tracks to be set
+ * for the first sample. As a result, the duration of that first sample may be larger.
*
- * @param writtenSamples All the written samples.
- * @param minInputPresentationTimestampUs The global minimum presentation timestamp which needs to
- * be subtracted from each sample's presentation timestamp.
+ * @param samplesInfo A list of {@linkplain BufferInfo sample info}.
+ * @param firstSamplePresentationTimeUs The presentation timestamp to override the first sample's
+ * presentation timestamp, in microseconds. This should be the minimum presentation timestamp
+ * across all tracks if the {@code samplesInfo} contains the first sample of the track.
+ * Otherwise this should be equal to the presentation timestamp of first sample present in the
+ * {@code samplesInfo} list.
* @param videoUnitTimescale The timescale of the track.
* @param lastDurationBehavior The behaviour for the last sample duration.
* @return A list of all the sample durations.
*/
// TODO: b/280084657 - Add support for setting last sample duration.
- public static List durationsVuForStts(
- List writtenSamples,
- long minInputPresentationTimestampUs,
+ public static List convertPresentationTimestampsToDurationsVu(
+ List samplesInfo,
+ long firstSamplePresentationTimeUs,
int videoUnitTimescale,
@Mp4Muxer.LastFrameDurationBehavior int lastDurationBehavior) {
- List durationsVu = new ArrayList<>();
+ List durationsVu = new ArrayList<>(samplesInfo.size());
- long currentTimeVu = 0L;
-
- for (int sampleId = 0; sampleId < writtenSamples.size(); sampleId++) {
- long samplePtsUs = writtenSamples.get(sampleId).presentationTimeUs;
- long sampleSpanEndsAtUs =
- sampleId == writtenSamples.size() - 1
- ? samplePtsUs
- : writtenSamples.get(sampleId + 1).presentationTimeUs;
-
- sampleSpanEndsAtUs -= minInputPresentationTimestampUs;
-
- long sampleSpanEndsAtVu = Mp4Utils.vuFromUs(sampleSpanEndsAtUs, videoUnitTimescale);
-
- long durationVu = sampleSpanEndsAtVu - currentTimeVu;
- currentTimeVu = sampleSpanEndsAtVu;
-
- if (durationVu >= Integer.MAX_VALUE) {
- throw new IllegalArgumentException(
- String.format(Locale.US, "Timestamp delta %d doesn't fit into an int", durationVu));
- }
-
- durationsVu.add(durationVu);
+ if (samplesInfo.isEmpty()) {
+ return durationsVu;
}
- adjustLastSampleDuration(durationsVu, lastDurationBehavior);
+ long currentSampleTimeUs = firstSamplePresentationTimeUs;
+ for (int nextSampleId = 1; nextSampleId < samplesInfo.size(); nextSampleId++) {
+ long nextSampleTimeUs = samplesInfo.get(nextSampleId).presentationTimeUs;
+ // TODO: b/316158030 - First calculate the duration and then convert us to vu to avoid
+ // rounding error.
+ long currentSampleDurationVu =
+ Mp4Utils.vuFromUs(nextSampleTimeUs, videoUnitTimescale)
+ - Mp4Utils.vuFromUs(currentSampleTimeUs, videoUnitTimescale);
+ if (currentSampleDurationVu > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException(
+ String.format(
+ Locale.US, "Timestamp delta %d doesn't fit into an int", currentSampleDurationVu));
+ }
+ durationsVu.add(currentSampleDurationVu);
+ currentSampleTimeUs = nextSampleTimeUs;
+ }
+ // Default duration for the last sample.
+ durationsVu.add(0L);
+ adjustLastSampleDuration(durationsVu, lastDurationBehavior);
return durationsVu;
}
@@ -803,16 +805,16 @@ import java.util.Locale;
return BoxUtils.wrapBoxesIntoBox("ftyp", boxBytes);
}
+ // TODO: b/317117431 - Change this method to getLastSampleDuration().
/** Adjusts the duration of the very last sample if needed. */
private static void adjustLastSampleDuration(
List durationsToBeAdjustedVu, @Mp4Muxer.LastFrameDurationBehavior int behavior) {
- // Technically, MP4 files store not timestamps but frame durations. Thus, if we interpret
- // timestamps as the start of frames then it's not obvious what's the duration of the very
- // last frame should be. If our samples follow each other in roughly regular intervals (e.g. in
- // a normal, 30 fps video), it makes sense to assume that the last sample will last the same ~33
- // ms as the all the other ones before. On the other hand, if we have just a few, irregularly
- // spaced frames, with duplication, the entire duration of the video will increase, creating
- // abnormal gaps.
+ // Technically, MP4 file stores frame durations, not timestamps. If a frame starts at a
+ // given timestamp then the duration of the last frame is not obvious. If samples follow each
+ // other in roughly regular intervals (e.g. in a normal, 30 fps video), it can be safely assumed
+ // that the last sample will have same duration (~33ms) as other samples. On the other hand, if
+ // there are just a few, irregularly spaced frames, with duplication, the entire duration of the
+ // video will increase, creating abnormal gaps.
if (durationsToBeAdjustedVu.size() <= 2) {
// Nothing to duplicate if there are 0 or 1 entries.
diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4MoovStructure.java b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4MoovStructure.java
index 921a41c487..dee215f30c 100644
--- a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4MoovStructure.java
+++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4MoovStructure.java
@@ -72,7 +72,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
// Generate the sample durations to calculate the total duration for tkhd box.
List sampleDurationsVu =
- Boxes.durationsVuForStts(
+ Boxes.convertPresentationTimestampsToDurationsVu(
track.writtenSamples(),
minInputPtsUs,
track.videoUnitTimebase(),
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 aebce1c5da..22fa860fd9 100644
--- a/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java
+++ b/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java
@@ -371,14 +371,14 @@ public class BoxesTest {
@Test
public void
- getDurationsVuForStts_singleSampleAtZeroTimestamp_lastFrameDurationShort_returnsSingleZeroLengthSample() {
+ convertPresentationTimestampsToDurationsVu_singleSampleAtZeroTimestamp_returnsSampleLengthEqualsZero() {
List sampleBufferInfos =
createBufferInfoListWithSamplePresentationTimestamps(0L);
List durationsVu =
- Boxes.durationsVuForStts(
+ Boxes.convertPresentationTimestampsToDurationsVu(
sampleBufferInfos,
- /* minInputPresentationTimestampUs= */ 0L,
+ /* firstSamplePresentationTimeUs= */ 0L,
VU_TIMEBASE,
LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME);
@@ -387,62 +387,30 @@ public class BoxesTest {
@Test
public void
- getDurationsVuForStts_singleSampleAtZeroTimestamp_lastFrameDurationDuplicate_returnsSingleZeroLengthSample() {
+ convertPresentationTimestampsToDurationsVu_singleSampleAtNonZeroTimestamp_returnsSampleLengthEqualsZero() {
List sampleBufferInfos =
- createBufferInfoListWithSamplePresentationTimestamps(0L);
+ createBufferInfoListWithSamplePresentationTimestamps(5_000L);
List durationsVu =
- Boxes.durationsVuForStts(
+ Boxes.convertPresentationTimestampsToDurationsVu(
sampleBufferInfos,
- /* minInputPresentationTimestampUs= */ 0L,
+ /* firstSamplePresentationTimeUs= */ 0L,
VU_TIMEBASE,
- LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION);
+ LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME);
assertThat(durationsVu).containsExactly(0L);
}
@Test
public void
- getDurationsVuForStts_singleSampleAtNonZeroTimestamp_lastFrameDurationShort_returnsSampleLengthEqualsTimestamp() {
- List sampleBufferInfos =
- createBufferInfoListWithSamplePresentationTimestamps(5_000L);
-
- List durationsVu =
- Boxes.durationsVuForStts(
- sampleBufferInfos,
- /* minInputPresentationTimestampUs= */ 0L,
- VU_TIMEBASE,
- LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME);
-
- assertThat(durationsVu).containsExactly(500L);
- }
-
- @Test
- public void
- getDurationsVuForStts_singleSampleAtNonZeroTimestamp_lastFrameDurationDuplicate_returnsSampleLengthEqualsTimestamp() {
- List sampleBufferInfos =
- createBufferInfoListWithSamplePresentationTimestamps(5_000L);
-
- List durationsVu =
- Boxes.durationsVuForStts(
- sampleBufferInfos,
- /* minInputPresentationTimestampUs= */ 0L,
- VU_TIMEBASE,
- LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION);
-
- assertThat(durationsVu).containsExactly(500L);
- }
-
- @Test
- public void
- getDurationsVuForStts_differentSampleDurations_lastFrameDurationShort_returnsLastSampleOfZeroDuration() {
+ convertPresentationTimestampsToDurationsVu_differentSampleDurations_lastFrameDurationShort_returnsLastSampleOfZeroDuration() {
List sampleBufferInfos =
createBufferInfoListWithSamplePresentationTimestamps(0L, 30_000L, 80_000L);
List durationsVu =
- Boxes.durationsVuForStts(
+ Boxes.convertPresentationTimestampsToDurationsVu(
sampleBufferInfos,
- /* minInputPresentationTimestampUs= */ 0L,
+ /* firstSamplePresentationTimeUs= */ 0L,
VU_TIMEBASE,
LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME);
@@ -451,14 +419,14 @@ public class BoxesTest {
@Test
public void
- getDurationsVuForStts_differentSampleDurations_lastFrameDurationDuplicate_returnsLastSampleOfDuplicateDuration() {
+ convertPresentationTimestampsToDurationsVu_differentSampleDurations_lastFrameDurationDuplicate_returnsLastSampleOfDuplicateDuration() {
List sampleBufferInfos =
createBufferInfoListWithSamplePresentationTimestamps(0L, 30_000L, 80_000L);
List durationsVu =
- Boxes.durationsVuForStts(
+ Boxes.convertPresentationTimestampsToDurationsVu(
sampleBufferInfos,
- /* minInputPresentationTimestampUs= */ 0L,
+ /* firstSamplePresentationTimeUs= */ 0L,
VU_TIMEBASE,
LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION);