Improve durationsVuForStts method implementation
Issues with the current implementation 1. The implementation is unnecessarily complicated and can be easily simplified.To make all the tracks start from the same time, its only the first sample that require some timestamp adjustments but the current implementation shifts all the timestamps. Since method calculates the `sample duration`, shifting all the timestamps has no effect as such. 2. The implementation always forced first sample to start at 0. But when we want to use same method for `Fragmented MP4` then it will look inaccurate as we will call this method for different `fragments` and each `fragment` will not start from 0 presentation time. Although the output will be same since this method returns `duration` and not the `timestamps`. 3. As per previous implementation if there is just one sample then its duration is made equals to its presentation time, which looks incorrect. With new changes, if a single sample is passed then its duration will always be 0 irrespective of specified last sample duration behaviour. PiperOrigin-RevId: 592826612
This commit is contained in:
parent
250fc80419
commit
0b88e09a82
@ -23,6 +23,7 @@ import static androidx.media3.muxer.ColorUtils.MEDIAFORMAT_TRANSFER_TO_MP4_TRANS
|
|||||||
import static androidx.media3.muxer.Mp4Utils.MVHD_TIMEBASE;
|
import static androidx.media3.muxer.Mp4Utils.MVHD_TIMEBASE;
|
||||||
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
|
import android.media.MediaCodec.BufferInfo;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.ColorInfo;
|
import androidx.media3.common.ColorInfo;
|
||||||
@ -345,7 +346,7 @@ import java.util.Locale;
|
|||||||
}
|
}
|
||||||
|
|
||||||
String locationString =
|
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);
|
ByteBuffer xyzBoxContents = ByteBuffer.allocate(locationString.length() + 2 + 2);
|
||||||
xyzBoxContents.putShort((short) (xyzBoxContents.capacity() - 4));
|
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)
|
* Converts sample presentation times (in microseconds) to sample durations (in timebase units).
|
||||||
* that will go into the stts box.
|
|
||||||
*
|
*
|
||||||
* <p>ISO/IEC 14496-12: 8.6.1.3.1 recommends each track starts at 0. Therefore, the first sample
|
* <p>All the tracks must start from the same time. If all the tracks do not start from the same
|
||||||
* presentation timestamp is set to 0 and the duration of that sample may be larger as a result.
|
* 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 samplesInfo A list of {@linkplain BufferInfo sample info}.
|
||||||
* @param minInputPresentationTimestampUs The global minimum presentation timestamp which needs to
|
* @param firstSamplePresentationTimeUs The presentation timestamp to override the first sample's
|
||||||
* be subtracted from each sample's presentation timestamp.
|
* 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 videoUnitTimescale The timescale of the track.
|
||||||
* @param lastDurationBehavior The behaviour for the last sample duration.
|
* @param lastDurationBehavior The behaviour for the last sample duration.
|
||||||
* @return A list of all the sample durations.
|
* @return A list of all the sample durations.
|
||||||
*/
|
*/
|
||||||
// TODO: b/280084657 - Add support for setting last sample duration.
|
// TODO: b/280084657 - Add support for setting last sample duration.
|
||||||
public static List<Long> durationsVuForStts(
|
public static List<Long> convertPresentationTimestampsToDurationsVu(
|
||||||
List<MediaCodec.BufferInfo> writtenSamples,
|
List<BufferInfo> samplesInfo,
|
||||||
long minInputPresentationTimestampUs,
|
long firstSamplePresentationTimeUs,
|
||||||
int videoUnitTimescale,
|
int videoUnitTimescale,
|
||||||
@Mp4Muxer.LastFrameDurationBehavior int lastDurationBehavior) {
|
@Mp4Muxer.LastFrameDurationBehavior int lastDurationBehavior) {
|
||||||
List<Long> durationsVu = new ArrayList<>();
|
List<Long> durationsVu = new ArrayList<>(samplesInfo.size());
|
||||||
|
|
||||||
long currentTimeVu = 0L;
|
if (samplesInfo.isEmpty()) {
|
||||||
|
return durationsVu;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return durationsVu;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -803,16 +805,16 @@ import java.util.Locale;
|
|||||||
return BoxUtils.wrapBoxesIntoBox("ftyp", boxBytes);
|
return BoxUtils.wrapBoxesIntoBox("ftyp", boxBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: b/317117431 - Change this method to getLastSampleDuration().
|
||||||
/** Adjusts the duration of the very last sample if needed. */
|
/** Adjusts the duration of the very last sample if needed. */
|
||||||
private static void adjustLastSampleDuration(
|
private static void adjustLastSampleDuration(
|
||||||
List<Long> durationsToBeAdjustedVu, @Mp4Muxer.LastFrameDurationBehavior int behavior) {
|
List<Long> durationsToBeAdjustedVu, @Mp4Muxer.LastFrameDurationBehavior int behavior) {
|
||||||
// Technically, MP4 files store not timestamps but frame durations. Thus, if we interpret
|
// Technically, MP4 file stores frame durations, not timestamps. If a frame starts at a
|
||||||
// timestamps as the start of frames then it's not obvious what's the duration of the very
|
// given timestamp then the duration of the last frame is not obvious. If samples follow each
|
||||||
// last frame should be. If our samples follow each other in roughly regular intervals (e.g. in
|
// other in roughly regular intervals (e.g. in a normal, 30 fps video), it can be safely assumed
|
||||||
// a normal, 30 fps video), it makes sense to assume that the last sample will last the same ~33
|
// that the last sample will have same duration (~33ms) as other samples. On the other hand, if
|
||||||
// ms as the all the other ones before. On the other hand, if we have just a few, irregularly
|
// there are just a few, irregularly spaced frames, with duplication, the entire duration of the
|
||||||
// spaced frames, with duplication, the entire duration of the video will increase, creating
|
// video will increase, creating abnormal gaps.
|
||||||
// abnormal gaps.
|
|
||||||
|
|
||||||
if (durationsToBeAdjustedVu.size() <= 2) {
|
if (durationsToBeAdjustedVu.size() <= 2) {
|
||||||
// Nothing to duplicate if there are 0 or 1 entries.
|
// Nothing to duplicate if there are 0 or 1 entries.
|
||||||
|
@ -72,7 +72,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
|
|||||||
|
|
||||||
// Generate the sample durations to calculate the total duration for tkhd box.
|
// Generate the sample durations to calculate the total duration for tkhd box.
|
||||||
List<Long> sampleDurationsVu =
|
List<Long> sampleDurationsVu =
|
||||||
Boxes.durationsVuForStts(
|
Boxes.convertPresentationTimestampsToDurationsVu(
|
||||||
track.writtenSamples(),
|
track.writtenSamples(),
|
||||||
minInputPtsUs,
|
minInputPtsUs,
|
||||||
track.videoUnitTimebase(),
|
track.videoUnitTimebase(),
|
||||||
|
@ -371,14 +371,14 @@ public class BoxesTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
getDurationsVuForStts_singleSampleAtZeroTimestamp_lastFrameDurationShort_returnsSingleZeroLengthSample() {
|
convertPresentationTimestampsToDurationsVu_singleSampleAtZeroTimestamp_returnsSampleLengthEqualsZero() {
|
||||||
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
||||||
createBufferInfoListWithSamplePresentationTimestamps(0L);
|
createBufferInfoListWithSamplePresentationTimestamps(0L);
|
||||||
|
|
||||||
List<Long> durationsVu =
|
List<Long> durationsVu =
|
||||||
Boxes.durationsVuForStts(
|
Boxes.convertPresentationTimestampsToDurationsVu(
|
||||||
sampleBufferInfos,
|
sampleBufferInfos,
|
||||||
/* minInputPresentationTimestampUs= */ 0L,
|
/* firstSamplePresentationTimeUs= */ 0L,
|
||||||
VU_TIMEBASE,
|
VU_TIMEBASE,
|
||||||
LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME);
|
LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME);
|
||||||
|
|
||||||
@ -387,62 +387,30 @@ public class BoxesTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
getDurationsVuForStts_singleSampleAtZeroTimestamp_lastFrameDurationDuplicate_returnsSingleZeroLengthSample() {
|
convertPresentationTimestampsToDurationsVu_singleSampleAtNonZeroTimestamp_returnsSampleLengthEqualsZero() {
|
||||||
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
||||||
createBufferInfoListWithSamplePresentationTimestamps(0L);
|
createBufferInfoListWithSamplePresentationTimestamps(5_000L);
|
||||||
|
|
||||||
List<Long> durationsVu =
|
List<Long> durationsVu =
|
||||||
Boxes.durationsVuForStts(
|
Boxes.convertPresentationTimestampsToDurationsVu(
|
||||||
sampleBufferInfos,
|
sampleBufferInfos,
|
||||||
/* minInputPresentationTimestampUs= */ 0L,
|
/* firstSamplePresentationTimeUs= */ 0L,
|
||||||
VU_TIMEBASE,
|
VU_TIMEBASE,
|
||||||
LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION);
|
LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME);
|
||||||
|
|
||||||
assertThat(durationsVu).containsExactly(0L);
|
assertThat(durationsVu).containsExactly(0L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
getDurationsVuForStts_singleSampleAtNonZeroTimestamp_lastFrameDurationShort_returnsSampleLengthEqualsTimestamp() {
|
convertPresentationTimestampsToDurationsVu_differentSampleDurations_lastFrameDurationShort_returnsLastSampleOfZeroDuration() {
|
||||||
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
|
||||||
createBufferInfoListWithSamplePresentationTimestamps(5_000L);
|
|
||||||
|
|
||||||
List<Long> 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<MediaCodec.BufferInfo> sampleBufferInfos =
|
|
||||||
createBufferInfoListWithSamplePresentationTimestamps(5_000L);
|
|
||||||
|
|
||||||
List<Long> 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() {
|
|
||||||
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
||||||
createBufferInfoListWithSamplePresentationTimestamps(0L, 30_000L, 80_000L);
|
createBufferInfoListWithSamplePresentationTimestamps(0L, 30_000L, 80_000L);
|
||||||
|
|
||||||
List<Long> durationsVu =
|
List<Long> durationsVu =
|
||||||
Boxes.durationsVuForStts(
|
Boxes.convertPresentationTimestampsToDurationsVu(
|
||||||
sampleBufferInfos,
|
sampleBufferInfos,
|
||||||
/* minInputPresentationTimestampUs= */ 0L,
|
/* firstSamplePresentationTimeUs= */ 0L,
|
||||||
VU_TIMEBASE,
|
VU_TIMEBASE,
|
||||||
LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME);
|
LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME);
|
||||||
|
|
||||||
@ -451,14 +419,14 @@ public class BoxesTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
getDurationsVuForStts_differentSampleDurations_lastFrameDurationDuplicate_returnsLastSampleOfDuplicateDuration() {
|
convertPresentationTimestampsToDurationsVu_differentSampleDurations_lastFrameDurationDuplicate_returnsLastSampleOfDuplicateDuration() {
|
||||||
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
List<MediaCodec.BufferInfo> sampleBufferInfos =
|
||||||
createBufferInfoListWithSamplePresentationTimestamps(0L, 30_000L, 80_000L);
|
createBufferInfoListWithSamplePresentationTimestamps(0L, 30_000L, 80_000L);
|
||||||
|
|
||||||
List<Long> durationsVu =
|
List<Long> durationsVu =
|
||||||
Boxes.durationsVuForStts(
|
Boxes.convertPresentationTimestampsToDurationsVu(
|
||||||
sampleBufferInfos,
|
sampleBufferInfos,
|
||||||
/* minInputPresentationTimestampUs= */ 0L,
|
/* firstSamplePresentationTimeUs= */ 0L,
|
||||||
VU_TIMEBASE,
|
VU_TIMEBASE,
|
||||||
LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION);
|
LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user