From b10f0332292e25417673f9819fa87f3bf99c7a63 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Mar 2018 07:37:46 -0800 Subject: [PATCH] Derive duration from sample table if missing from track header Issue: #3926 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=188020756 --- RELEASENOTES.md | 2 + .../exoplayer2/extractor/mp4/AtomParsers.java | 79 ++++++++++++------- .../mp4/FixedSampleSizeRechunker.java | 13 ++- .../extractor/mp4/Mp4Extractor.java | 5 +- .../extractor/mp4/TrackSampleTable.java | 15 +++- 5 files changed, 79 insertions(+), 35 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f21ad7f392..3223d6ae4c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,8 @@ `Timeline.Window.windowStartTimeMs` field ([#3865](https://github.com/google/ExoPlayer/issues/3865), [#3888](https://github.com/google/ExoPlayer/issues/3888)). +* Enable seeking in MP4 streams where duration is set incorrectly in the track + header ([#3926](https://github.com/google/ExoPlayer/issues/3926)). ### 2.7.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 588282bc9b..37cfce7c7c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -49,7 +49,6 @@ import java.util.List; private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl"); private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); - private static final int TYPE_cenc = Util.getIntegerCodeForString("cenc"); private static final int TYPE_meta = Util.getIntegerCodeForString("meta"); /** @@ -128,7 +127,8 @@ import java.util.List; int sampleCount = sampleSizeBox.getSampleCount(); if (sampleCount == 0) { - return new TrackSampleTable(new long[0], new int[0], 0, new long[0], new int[0]); + return new TrackSampleTable( + new long[0], new int[0], 0, new long[0], new int[0], C.TIME_UNSET); } // Entries are byte offsets of chunks. @@ -193,6 +193,7 @@ import java.util.List; long[] timestamps; int[] flags; long timestampTimeUnits = 0; + long duration; if (!isRechunkable) { offsets = new long[sampleCount]; @@ -260,6 +261,7 @@ import java.util.List; offset += sizes[i]; remainingSamplesInChunk--; } + duration = timestampTimeUnits + timestampOffset; Assertions.checkArgument(remainingSamplesAtTimestampOffset == 0); // Remove trailing ctts entries with 0-valued sample counts. @@ -294,13 +296,15 @@ import java.util.List; maximumSize = rechunkedResults.maximumSize; timestamps = rechunkedResults.timestamps; flags = rechunkedResults.flags; + duration = rechunkedResults.duration; } + long durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, track.timescale); if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) { // There is no edit list, or we are ignoring it as we already have gapless metadata to apply. // This implementation does not support applying both gapless metadata and an edit list. Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); + return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); } // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a @@ -317,10 +321,11 @@ import java.util.List; long editStartTime = track.editListMediaTimes[0]; long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0], track.timescale, track.movieTimescale); - long lastSampleEndTime = timestampTimeUnits; - if (timestamps[0] <= editStartTime && editStartTime < timestamps[1] - && timestamps[timestamps.length - 1] < editEndTime && editEndTime <= lastSampleEndTime) { - long paddingTimeUnits = lastSampleEndTime - editEndTime; + if (timestamps[0] <= editStartTime + && editStartTime < timestamps[1] + && timestamps[timestamps.length - 1] < editEndTime + && editEndTime <= duration) { + long paddingTimeUnits = duration - editEndTime; long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0], track.format.sampleRate, track.timescale); long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits, @@ -330,7 +335,7 @@ import java.util.List; gaplessInfoHolder.encoderDelay = (int) encoderDelay; gaplessInfoHolder.encoderPadding = (int) encoderPadding; Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); + return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); } } } @@ -339,11 +344,15 @@ import java.util.List; // The current version of the spec leaves handling of an edit with zero segment_duration in // unfragmented files open to interpretation. We handle this as a special case and include all // samples in the edit. + long editStartTime = track.editListMediaTimes[0]; for (int i = 0; i < timestamps.length; i++) { - timestamps[i] = Util.scaleLargeTimestamp(timestamps[i] - track.editListMediaTimes[0], - C.MICROS_PER_SECOND, track.timescale); + timestamps[i] = + Util.scaleLargeTimestamp( + timestamps[i] - editStartTime, C.MICROS_PER_SECOND, track.timescale); } - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); + durationUs = + Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale); + return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); } // Omit any sample at the end point of an edit for audio tracks. @@ -354,13 +363,15 @@ import java.util.List; int nextSampleIndex = 0; boolean copyMetadata = false; for (int i = 0; i < track.editListDurations.length; i++) { - long mediaTime = track.editListMediaTimes[i]; - if (mediaTime != -1) { - long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale, - track.movieTimescale); - int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample, - false); + long editMediaTime = track.editListMediaTimes[i]; + if (editMediaTime != -1) { + long editDuration = + Util.scaleLargeTimestamp( + track.editListDurations[i], track.timescale, track.movieTimescale); + int startIndex = Util.binarySearchCeil(timestamps, editMediaTime, true, true); + int endIndex = + Util.binarySearchCeil( + timestamps, editMediaTime + editDuration, omitClippedSample, false); editedSampleCount += endIndex - startIndex; copyMetadata |= nextSampleIndex != startIndex; nextSampleIndex = endIndex; @@ -377,12 +388,13 @@ import java.util.List; long pts = 0; int sampleIndex = 0; for (int i = 0; i < track.editListDurations.length; i++) { - long mediaTime = track.editListMediaTimes[i]; - long duration = track.editListDurations[i]; - if (mediaTime != -1) { - long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale, - track.movieTimescale); - int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); + long editMediaTime = track.editListMediaTimes[i]; + long editDuration = track.editListDurations[i]; + if (editMediaTime != -1) { + long endMediaTime = + editMediaTime + + Util.scaleLargeTimestamp(editDuration, track.timescale, track.movieTimescale); + int startIndex = Util.binarySearchCeil(timestamps, editMediaTime, true, true); int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false); if (copyMetadata) { int count = endIndex - startIndex; @@ -392,8 +404,9 @@ import java.util.List; } for (int j = startIndex; j < endIndex; j++) { long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); - long timeInSegmentUs = Util.scaleLargeTimestamp(timestamps[j] - mediaTime, - C.MICROS_PER_SECOND, track.timescale); + long timeInSegmentUs = + Util.scaleLargeTimestamp( + timestamps[j] - editMediaTime, C.MICROS_PER_SECOND, track.timescale); editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs; if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) { editedMaximumSize = sizes[j]; @@ -401,8 +414,9 @@ import java.util.List; sampleIndex++; } } - pts += duration; + pts += editDuration; } + long editedDurationUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.timescale); boolean hasSyncSample = false; for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) { @@ -413,11 +427,16 @@ import java.util.List; // Such edit lists are often (although not always) broken, so we ignore it and continue. Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample."); Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); + return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); } - return new TrackSampleTable(editedOffsets, editedSizes, editedMaximumSize, editedTimestamps, - editedFlags); + return new TrackSampleTable( + editedOffsets, + editedSizes, + editedMaximumSize, + editedTimestamps, + editedFlags, + editedDurationUs); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java index 5dd6c6ea9f..8336a280a2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java @@ -33,13 +33,21 @@ import com.google.android.exoplayer2.util.Util; public final int maximumSize; public final long[] timestamps; public final int[] flags; + public final long duration; - private Results(long[] offsets, int[] sizes, int maximumSize, long[] timestamps, int[] flags) { + private Results( + long[] offsets, + int[] sizes, + int maximumSize, + long[] timestamps, + int[] flags, + long duration) { this.offsets = offsets; this.sizes = sizes; this.maximumSize = maximumSize; this.timestamps = timestamps; this.flags = flags; + this.duration = duration; } } @@ -95,8 +103,9 @@ import com.google.android.exoplayer2.util.Util; newSampleIndex++; } } + long duration = timestampDeltaInTimeUnits * originalSampleIndex; - return new Results(offsets, sizes, maximumSize, timestamps, flags); + return new Results(offsets, sizes, maximumSize, timestamps, flags, duration); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 112c2d1ba0..75bd2c16ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -427,7 +427,10 @@ public final class Mp4Extractor implements Extractor, SeekMap { } mp4Track.trackOutput.format(format); - durationUs = Math.max(durationUs, track.durationUs); + durationUs = + Math.max( + durationUs, + track.durationUs != C.TIME_UNSET ? track.durationUs : trackSampleTable.durationUs); if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) { firstVideoTrackIndex = tracks.size(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java index cf479eaf3e..9f77c49664 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java @@ -48,9 +48,19 @@ import com.google.android.exoplayer2.util.Util; * Sample flags. */ public final int[] flags; + /** + * The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample + * table is empty. + */ + public final long durationUs; - public TrackSampleTable(long[] offsets, int[] sizes, int maximumSize, long[] timestampsUs, - int[] flags) { + public TrackSampleTable( + long[] offsets, + int[] sizes, + int maximumSize, + long[] timestampsUs, + int[] flags, + long durationUs) { Assertions.checkArgument(sizes.length == timestampsUs.length); Assertions.checkArgument(offsets.length == timestampsUs.length); Assertions.checkArgument(flags.length == timestampsUs.length); @@ -60,6 +70,7 @@ import com.google.android.exoplayer2.util.Util; this.maximumSize = maximumSize; this.timestampsUs = timestampsUs; this.flags = flags; + this.durationUs = durationUs; sampleCount = offsets.length; }