diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 86ddada1f07..09c920ddda5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,8 @@ * Add workaround for track index mismatches between tfhd and tkhd boxes in fragmented MP4 files ([#4083](https://github.com/google/ExoPlayer/issues/4083)). +* Ignore all MP4 edit lists if one edit list couldn't be handled + ([#4348](https://github.com/google/ExoPlayer/issues/4348)). * Fix issue when switching track selection from an embedded track to a primary track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)). 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 a2b787d6b01..d11914919ad 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 @@ -43,6 +43,9 @@ */ /* package */ final class AtomParsers { + /** Thrown if an edit list couldn't be applied. */ + public static final class UnhandledEditListException extends ParserException {} + private static final String TAG = "AtomParsers"; private static final int TYPE_vide = Util.getIntegerCodeForString("vide"); @@ -117,10 +120,12 @@ public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long * @param stblAtom stbl (sample table) atom to decode. * @param gaplessInfoHolder Holder to populate with gapless playback information. * @return Sample table described by the stbl atom. - * @throws ParserException If the resulting sample sequence does not contain a sync sample. + * @throws UnhandledEditListException Thrown if the edit list can't be applied. + * @throws ParserException Thrown if the stbl atom can't be parsed. */ - public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom, - GaplessInfoHolder gaplessInfoHolder) throws ParserException { + public static TrackSampleTable parseStbl( + Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder) + throws ParserException { SampleSizeBox sampleSizeBox; Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); if (stszAtom != null) { @@ -136,7 +141,13 @@ public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAto int sampleCount = sampleSizeBox.getSampleCount(); if (sampleCount == 0) { return new TrackSampleTable( - new long[0], new int[0], 0, new long[0], new int[0], C.TIME_UNSET); + track, + /* offsets= */ new long[0], + /* sizes= */ new int[0], + /* maximumSize= */ 0, + /* timestampsUs= */ new long[0], + /* flags= */ new int[0], + /* durationUs= */ C.TIME_UNSET); } // Entries are byte offsets of chunks. @@ -315,7 +326,8 @@ public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAto // 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, durationUs); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); } // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a @@ -342,7 +354,8 @@ public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAto 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, durationUs); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); } } } @@ -359,7 +372,8 @@ public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAto } durationUs = Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); } // Omit any sample at the end point of an edit for audio tracks. @@ -409,6 +423,11 @@ public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAto System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count); System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count); } + if (startIndex < endIndex && (editedFlags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) == 0) { + // Applying the edit list would require prerolling from a sync sample. + Log.w(TAG, "Ignoring edit list: edit does not start with a sync sample."); + throw new UnhandledEditListException(); + } for (int j = startIndex; j < endIndex; j++) { long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); long timeInSegmentUs = @@ -424,20 +443,8 @@ public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAto 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++) { - hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0; - } - if (!hasSyncSample) { - // We don't support edit lists where the edited sample sequence doesn't contain a sync sample. - // 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, durationUs); - } - return new TrackSampleTable( + track, editedOffsets, editedSizes, editedMaximumSize, 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 e70a49a2d70..1b455ab9e24 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 @@ -391,25 +391,21 @@ private void processMoovAtom(ContainerAtom moov) throws ParserException { } } - for (int i = 0; i < moov.containerChildren.size(); i++) { - Atom.ContainerAtom atom = moov.containerChildren.get(i); - if (atom.type != Atom.TYPE_trak) { - continue; - } - - Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), - C.TIME_UNSET, null, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, isQuickTime); - if (track == null) { - continue; - } - - Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) - .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); - TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); - if (trackSampleTable.sampleCount == 0) { - continue; - } + boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0; + ArrayList trackSampleTables; + try { + trackSampleTables = getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists); + } catch (AtomParsers.UnhandledEditListException e) { + // Discard gapless info as we aren't able to handle corresponding edits. + gaplessInfoHolder = new GaplessInfoHolder(); + trackSampleTables = + getTrackSampleTables(moov, gaplessInfoHolder, /* ignoreEditLists= */ true); + } + int trackCount = trackSampleTables.size(); + for (int i = 0; i < trackCount; i++) { + TrackSampleTable trackSampleTable = trackSampleTables.get(i); + Track track = trackSampleTable.track; Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i, track.type)); // Each sample has up to three bytes of overhead for the start code that replaces its length. @@ -445,6 +441,39 @@ private void processMoovAtom(ContainerAtom moov) throws ParserException { extractorOutput.seekMap(this); } + private ArrayList getTrackSampleTables( + ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists) + throws ParserException { + ArrayList trackSampleTables = new ArrayList<>(); + for (int i = 0; i < moov.containerChildren.size(); i++) { + Atom.ContainerAtom atom = moov.containerChildren.get(i); + if (atom.type != Atom.TYPE_trak) { + continue; + } + Track track = + AtomParsers.parseTrak( + atom, + moov.getLeafAtomOfType(Atom.TYPE_mvhd), + /* duration= */ C.TIME_UNSET, + /* drmInitData= */ null, + ignoreEditLists, + isQuickTime); + if (track == null) { + continue; + } + Atom.ContainerAtom stblAtom = + atom.getContainerAtomOfType(Atom.TYPE_mdia) + .getContainerAtomOfType(Atom.TYPE_minf) + .getContainerAtomOfType(Atom.TYPE_stbl); + TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); + if (trackSampleTable.sampleCount == 0) { + continue; + } + trackSampleTables.add(trackSampleTable); + } + return trackSampleTables; + } + /** * Attempts to extract the next sample in the current mdat atom for the specified track. *

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 9f77c496646..56851fc1e09 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 @@ -24,29 +24,19 @@ */ /* package */ final class TrackSampleTable { - /** - * Number of samples. - */ + /** The track corresponding to this sample table. */ + public final Track track; + /** Number of samples. */ public final int sampleCount; - /** - * Sample offsets in bytes. - */ + /** Sample offsets in bytes. */ public final long[] offsets; - /** - * Sample sizes in bytes. - */ + /** Sample sizes in bytes. */ public final int[] sizes; - /** - * Maximum sample size in {@link #sizes}. - */ + /** Maximum sample size in {@link #sizes}. */ public final int maximumSize; - /** - * Sample timestamps in microseconds. - */ + /** Sample timestamps in microseconds. */ public final long[] timestampsUs; - /** - * Sample flags. - */ + /** Sample flags. */ public final int[] flags; /** * The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample @@ -55,6 +45,7 @@ public final long durationUs; public TrackSampleTable( + Track track, long[] offsets, int[] sizes, int maximumSize, @@ -65,6 +56,7 @@ public TrackSampleTable( Assertions.checkArgument(offsets.length == timestampsUs.length); Assertions.checkArgument(flags.length == timestampsUs.length); + this.track = track; this.offsets = offsets; this.sizes = sizes; this.maximumSize = maximumSize;