From 37e00c8c6f278009e236f4c8606fe2d9dcac1073 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Mar 2016 05:17:48 -0700 Subject: [PATCH] Support multiplexed fMP4. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=117228173 --- .../exoplayer/extractor/ChunkIndex.java | 6 +- .../extractor/mp4/FragmentedMp4Extractor.java | 461 +++++++++++------- .../SmoothStreamingChunkSource.java | 5 +- 3 files changed, 299 insertions(+), 173 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ChunkIndex.java b/library/src/main/java/com/google/android/exoplayer/extractor/ChunkIndex.java index 6e18adc8ef..2a173f8f72 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ChunkIndex.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ChunkIndex.java @@ -50,20 +50,18 @@ public final class ChunkIndex implements SeekMap { private final long durationUs; /** - * @param durationUs The duration of the stream. * @param sizes The chunk sizes, in bytes. * @param offsets The chunk byte offsets. * @param durationsUs The chunk durations, in microseconds. * @param timesUs The start time of each chunk, in microseconds. */ - public ChunkIndex(long durationUs, int[] sizes, long[] offsets, long[] durationsUs, - long[] timesUs) { - this.durationUs = durationUs; + public ChunkIndex(int[] sizes, long[] offsets, long[] durationsUs, long[] timesUs) { this.sizes = sizes; this.offsets = offsets; this.durationsUs = durationsUs; this.timesUs = timesUs; length = sizes.length; + durationUs = durationsUs[length - 1] + timesUs[length - 1]; } /** diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java index 1a3f5c005c..578c977b7a 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java @@ -28,12 +28,15 @@ import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer.extractor.mp4.Atom.LeafAtom; +import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.NalUnitUtil; import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.Util; import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; import java.io.IOException; import java.util.Arrays; @@ -43,8 +46,6 @@ import java.util.UUID; /** * Facilitates the extraction of data from the fragmented mp4 container format. - *

- * This implementation only supports de-muxed (i.e. single track) streams. */ public final class FragmentedMp4Extractor implements Extractor { @@ -57,35 +58,45 @@ public final class FragmentedMp4Extractor implements Extractor { *

* This flag does nothing if the stream is not a video stream. */ - public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1; + public static final int FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1; /** * Flag to ignore any tfdt boxes in the stream. */ - public static final int WORKAROUND_IGNORE_TFDT_BOX = 2; + public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2; + + /** + * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 + * container. + */ + private static final int FLAG_SIDELOADED = 4; private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; - // Parser states + // Parser states. private static final int STATE_READING_ATOM_HEADER = 0; private static final int STATE_READING_ATOM_PAYLOAD = 1; private static final int STATE_READING_ENCRYPTION_DATA = 2; private static final int STATE_READING_SAMPLE_START = 3; private static final int STATE_READING_SAMPLE_CONTINUE = 4; - private final int workaroundFlags; + // Workarounds. + private final int flags; + private final Track sideloadedTrack; + + // Track-linked data bundle, accessible as a whole through trackID. + private final SparseArray trackBundles; // Temporary arrays. private final ParsableByteArray nalStartCode; private final ParsableByteArray nalLength; private final ParsableByteArray encryptionSignalByte; - // Parser state + // Parser state. private final ParsableByteArray atomHeader; private final byte[] extendedTypeScratch; private final Stack containerAtoms; - private final TrackFragment fragmentRun; private int parserState; private int atomType; @@ -94,18 +105,13 @@ public final class FragmentedMp4Extractor implements Extractor { private ParsableByteArray atomData; private long endOfMdatPosition; - private int sampleIndex; + private TrackBundle currentTrackBundle; private int sampleSize; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; - // Data parsed from moov atom. - private Track track; - private DefaultSampleValues extendsDefaults; - - // Extractor outputs. + // Extractor output. private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; // Whether extractorOutput.seekMap has been invoked. private boolean haveOutputSeekMap; @@ -115,18 +121,27 @@ public final class FragmentedMp4Extractor implements Extractor { } /** - * @param workaroundFlags Flags to allow parsing of faulty streams. - * {@link #WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME} is currently the only flag defined. + * @param flags Flags to allow parsing of faulty streams. */ - public FragmentedMp4Extractor(int workaroundFlags) { - this.workaroundFlags = workaroundFlags; + public FragmentedMp4Extractor(int flags) { + this(flags, null); + } + + /** + * @param flags Flags to allow parsing of faulty streams. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor + * will not receive a moov box in the input data. + */ + public FragmentedMp4Extractor(int flags, Track sideloadedTrack) { + this.sideloadedTrack = sideloadedTrack; + this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); encryptionSignalByte = new ParsableByteArray(1); extendedTypeScratch = new byte[16]; containerAtoms = new Stack<>(); - fragmentRun = new TrackFragment(); + trackBundles = new SparseArray<>(); enterReadingAtomHeaderState(); } @@ -135,25 +150,14 @@ public final class FragmentedMp4Extractor implements Extractor { return Sniffer.sniffFragmented(input); } - /** - * Sideloads track information into the extractor. - *

- * Should be called before {@link #read(ExtractorInput, PositionHolder)} in the case that the - * extractor will not receive a moov atom in the input data, from which track information would - * normally be parsed. - * - * @param track The track to sideload. - */ - public void setTrack(Track track) { - this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0); - this.track = track; - } - @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = output.track(0); - extractorOutput.endTracks(); + if (sideloadedTrack != null) { + trackBundles.put(0, new TrackBundle(sideloadedTrack, new TrackFragment(), output.track(0), + new DefaultSampleValues(0, 0, 0, 0))); + extractorOutput.endTracks(); + } } @Override @@ -214,21 +218,22 @@ public final class FragmentedMp4Extractor implements Extractor { long atomPosition = input.getPosition() - atomHeaderBytesRead; if (atomType == Atom.TYPE_moof) { // The data positions may be updated when parsing the tfhd/trun. - fragmentRun.auxiliaryDataPosition = atomPosition; - fragmentRun.dataPosition = atomPosition; + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + TrackFragment fragment = trackBundles.valueAt(i).fragment; + fragment.auxiliaryDataPosition = atomPosition; + fragment.dataPosition = atomPosition; + } } if (atomType == Atom.TYPE_mdat) { + currentTrackBundle = null; endOfMdatPosition = atomPosition + atomSize; if (!haveOutputSeekMap) { - extractorOutput.seekMap(new SeekMap.Unseekable(track.durationUs)); + extractorOutput.seekMap(new SeekMap.Unseekable(C.UNKNOWN_TIME_US)); haveOutputSeekMap = true; } - if (fragmentRun.sampleEncryptionDataNeedsFill) { - parserState = STATE_READING_ENCRYPTION_DATA; - } else { - parserState = STATE_READING_SAMPLE_START; - } + parserState = STATE_READING_ENCRYPTION_DATA; return true; } @@ -276,7 +281,7 @@ public final class FragmentedMp4Extractor implements Extractor { if (!containerAtoms.isEmpty()) { containerAtoms.peek().add(leaf); } else if (leaf.type == Atom.TYPE_sidx) { - ChunkIndex segmentIndex = parseSidx(track.durationUs, leaf.data, inputPosition); + ChunkIndex segmentIndex = parseSidx(leaf.data, inputPosition); extractorOutput.seekMap(segmentIndex); haveOutputSeekMap = true; } @@ -292,13 +297,14 @@ public final class FragmentedMp4Extractor implements Extractor { } } - private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { - List moovChildren = moov.leafChildren; - int moovChildrenSize = moovChildren.size(); + private void onMoovContainerAtomRead(ContainerAtom moov) { + Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); + List moovLeafChildren = moov.leafChildren; + int moovLeafChildrenSize = moovLeafChildren.size(); DrmInitData.Mapped drmInitData = null; - for (int i = 0; i < moovChildrenSize; i++) { - LeafAtom child = moovChildren.get(i); + for (int i = 0; i < moovLeafChildrenSize; i++) { + LeafAtom child = moovLeafChildren.get(i); if (child.type == Atom.TYPE_pssh) { if (drmInitData == null) { drmInitData = new DrmInitData.Mapped(); @@ -317,90 +323,119 @@ public final class FragmentedMp4Extractor implements Extractor { extractorOutput.drmInitData(drmInitData); } + // Read declaration of Track Fragments in the Moov box. ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); - extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).data); - track = AtomParsers.parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak), - moov.getLeafAtomOfType(Atom.TYPE_mvhd), false); - if (track == null) { - throw new ParserException("Track type not supported."); + SparseArray defaultSampleValuesArray = new SparseArray<>(); + int mvexChildrenSize = mvex.leafChildren.size(); + for (int i = 0; i < mvexChildrenSize; i++) { + Atom.LeafAtom atom = mvex.leafChildren.get(i); + if (atom.type == Atom.TYPE_trex) { + Pair trexData = parseTrex(atom.data); + defaultSampleValuesArray.put(trexData.first, trexData.second); + } } - trackOutput.format(track.format); + + // Construction of Tracks and TrackOutputs. + trackBundles.clear(); + int moovContainerChildrenSize = moov.containerChildren.size(); + int trackBundlesSize = 0; + for (int i = 0; i < moovContainerChildrenSize; i++) { + Atom.ContainerAtom atom = moov.containerChildren.get(i); + if (atom.type == Atom.TYPE_trak) { + Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), false); + if (track != null) { + DefaultSampleValues defaultSampleValues = defaultSampleValuesArray.get(track.id); + TrackBundle bundle = new TrackBundle(track, new TrackFragment(), + extractorOutput.track(trackBundlesSize++), defaultSampleValues); + bundle.output.format(track.format); + trackBundles.put(track.id, bundle); + } + } + } + extractorOutput.endTracks(); } private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { - fragmentRun.reset(); - parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags, extendedTypeScratch); - sampleIndex = 0; + parseMoof(moof, trackBundles, flags, extendedTypeScratch); } /** * Parses a trex atom (defined in 14496-12). */ - private static DefaultSampleValues parseTrex(ParsableByteArray trex) { - trex.setPosition(Atom.FULL_HEADER_SIZE + 4); + private static Pair parseTrex(ParsableByteArray trex) { + trex.setPosition(Atom.FULL_HEADER_SIZE); + int trackId = trex.readInt(); int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1; int defaultSampleDuration = trex.readUnsignedIntToInt(); int defaultSampleSize = trex.readUnsignedIntToInt(); int defaultSampleFlags = trex.readInt(); - return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration, - defaultSampleSize, defaultSampleFlags); + + return Pair.create(trackId, new DefaultSampleValues(defaultSampleDescriptionIndex, + defaultSampleDuration, defaultSampleSize, defaultSampleFlags)); } - private static void parseMoof(Track track, DefaultSampleValues extendsDefaults, - ContainerAtom moof, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) - throws ParserException { - if (moof.getChildAtomOfTypeCount(Atom.TYPE_traf) != 1) { - throw new ParserException("Traf count in moof != 1 (unsupported)."); + private static void parseMoof(ContainerAtom moof, SparseArray trackBundleArray, + int flags, byte[] extendedTypeScratch) throws ParserException { + int moofContainerChildrenSize = moof.containerChildren.size(); + for (int i = 0; i < moofContainerChildrenSize; i++) { + Atom.ContainerAtom child = moof.containerChildren.get(i); + if (child.type == Atom.TYPE_traf) { + parseTraf(child, trackBundleArray, flags, extendedTypeScratch); + } } - parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf), - out, workaroundFlags, extendedTypeScratch); } /** * Parses a traf atom (defined in 14496-12). */ - private static void parseTraf(Track track, DefaultSampleValues extendsDefaults, - ContainerAtom traf, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) - throws ParserException { + private static void parseTraf(ContainerAtom traf, SparseArray trackBundleArray, + int flags, byte[] extendedTypeScratch) throws ParserException { if (traf.getChildAtomOfTypeCount(Atom.TYPE_trun) != 1) { throw new ParserException("Trun count in traf != 1 (unsupported)."); } + + LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); + TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray, flags); + if (trackBundle == null) { + return; + } + TrackFragment fragment = trackBundle.fragment; + trackBundle.currentSampleIndex = 0; + fragment.reset(); + LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); long decodeTime; - if (tfdtAtom == null || (workaroundFlags & WORKAROUND_IGNORE_TFDT_BOX) != 0) { + if (tfdtAtom == null || (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) != 0) { decodeTime = 0; } else { decodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data); } - LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); - parseTfhd(extendsDefaults, tfhd.data, out); - LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun); - parseTrun(track, out.header, decodeTime, workaroundFlags, trun.data, out); + parseTrun(trackBundle, decodeTime, flags, trun.data); LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); if (saiz != null) { - TrackEncryptionBox trackEncryptionBox = - track.sampleDescriptionEncryptionBoxes[out.header.sampleDescriptionIndex]; - parseSaiz(trackEncryptionBox, saiz.data, out); + TrackEncryptionBox trackEncryptionBox = trackBundle.track + .sampleDescriptionEncryptionBoxes[fragment.header.sampleDescriptionIndex]; + parseSaiz(trackEncryptionBox, saiz.data, fragment); } LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); if (saio != null) { - parseSaio(saio.data, out); + parseSaio(saio.data, fragment); } LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); if (senc != null) { - parseSenc(senc.data, out); + parseSenc(senc.data, fragment); } int childrenSize = traf.leafChildren.size(); for (int i = 0; i < childrenSize; i++) { LeafAtom atom = traf.leafChildren.get(i); if (atom.type == Atom.TYPE_uuid) { - parseUuid(atom.data, out, extendedTypeScratch); + parseUuid(atom.data, fragment, extendedTypeScratch); } } } @@ -441,7 +476,7 @@ public final class FragmentedMp4Extractor implements Extractor { * Parses a saio atom (defined in 14496-12). * * @param saio The saio atom to parse. - * @param out The track fragment to populate with data from the saio atom. + * @param out The {@link TrackFragment} to populate with data from the saio atom. */ private static void parseSaio(ParsableByteArray saio, TrackFragment out) throws ParserException { saio.setPosition(Atom.HEADER_SIZE); @@ -463,36 +498,44 @@ public final class FragmentedMp4Extractor implements Extractor { } /** - * Parses a tfhd atom (defined in 14496-12). + * Parses a tfhd atom (defined in 14496-12), updates the corresponding {@link TrackFragment} and + * returns the {@link TrackBundle} of the corresponding {@link Track}. If the tfhd does not refer + * to any {@link TrackBundle}, {@code null} is returned and no changes are made. * - * @param extendsDefaults Default sample values from the trex atom. * @param tfhd The tfhd atom to parse. - * @param out The track fragment to populate with data from the tfhd atom. + * @param trackBundles The track bundles, one of which corresponds to the tfhd atom being parsed. + * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd + * does not refer to any {@link TrackBundle}. */ - private static void parseTfhd(DefaultSampleValues extendsDefaults, ParsableByteArray tfhd, - TrackFragment out) { + private static TrackBundle parseTfhd(ParsableByteArray tfhd, + SparseArray trackBundles, int flags) { tfhd.setPosition(Atom.HEADER_SIZE); int fullAtom = tfhd.readInt(); - int flags = Atom.parseFullAtomFlags(fullAtom); - - tfhd.skipBytes(4); // trackId - if ((flags & 0x01 /* base_data_offset_present */) != 0) { + int atomFlags = Atom.parseFullAtomFlags(fullAtom); + int trackId = tfhd.readInt(); + TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0); + if (trackBundle == null) { + return null; + } + if ((atomFlags & 0x01 /* base_data_offset_present */) != 0) { long baseDataPosition = tfhd.readUnsignedLongToLong(); - out.dataPosition = baseDataPosition; - out.auxiliaryDataPosition = baseDataPosition; + trackBundle.fragment.dataPosition = baseDataPosition; + trackBundle.fragment.auxiliaryDataPosition = baseDataPosition; } + DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues; int defaultSampleDescriptionIndex = - ((flags & 0x02 /* default_sample_description_index_present */) != 0) - ? tfhd.readUnsignedIntToInt() - 1 : extendsDefaults.sampleDescriptionIndex; - int defaultSampleDuration = ((flags & 0x08 /* default_sample_duration_present */) != 0) - ? tfhd.readUnsignedIntToInt() : extendsDefaults.duration; - int defaultSampleSize = ((flags & 0x10 /* default_sample_size_present */) != 0) - ? tfhd.readUnsignedIntToInt() : extendsDefaults.size; - int defaultSampleFlags = ((flags & 0x20 /* default_sample_flags_present */) != 0) - ? tfhd.readUnsignedIntToInt() : extendsDefaults.flags; - out.header = new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration, - defaultSampleSize, defaultSampleFlags); + ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0) + ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex; + int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) + ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration; + int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0) + ? tfhd.readUnsignedIntToInt() : defaultSampleValues.size; + int defaultSampleFlags = ((atomFlags & 0x20 /* default_sample_flags_present */) != 0) + ? tfhd.readUnsignedIntToInt() : defaultSampleValues.flags; + trackBundle.fragment.header = new DefaultSampleValues(defaultSampleDescriptionIndex, + defaultSampleDuration, defaultSampleSize, defaultSampleFlags); + return trackBundle; } /** @@ -511,34 +554,38 @@ public final class FragmentedMp4Extractor implements Extractor { /** * Parses a trun atom (defined in 14496-12). * - * @param track The corresponding track. - * @param defaultSampleValues Default sample values. - * @param decodeTime The decode time. + * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into + * which parsed data should be placed. + * @param decodeTime The decode time of the first sample in the fragment run. + * @param flags Flags to allow any required workaround to be executed. * @param trun The trun atom to parse. - * @param out The {@TrackFragment} into which parsed data should be placed. */ - private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues, - long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) { + private static void parseTrun(TrackBundle trackBundle, long decodeTime, int flags, + ParsableByteArray trun) { trun.setPosition(Atom.HEADER_SIZE); int fullAtom = trun.readInt(); - int flags = Atom.parseFullAtomFlags(fullAtom); + int atomFlags = Atom.parseFullAtomFlags(fullAtom); + + Track track = trackBundle.track; + TrackFragment fragment = trackBundle.fragment; + DefaultSampleValues defaultSampleValues = fragment.header; int sampleCount = trun.readUnsignedIntToInt(); - if ((flags & 0x01 /* data_offset_present */) != 0) { - out.dataPosition += trun.readInt(); + if ((atomFlags & 0x01 /* data_offset_present */) != 0) { + fragment.dataPosition += trun.readInt(); } - boolean firstSampleFlagsPresent = (flags & 0x04 /* first_sample_flags_present */) != 0; + boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0; int firstSampleFlags = defaultSampleValues.flags; if (firstSampleFlagsPresent) { firstSampleFlags = trun.readUnsignedIntToInt(); } - boolean sampleDurationsPresent = (flags & 0x100 /* sample_duration_present */) != 0; - boolean sampleSizesPresent = (flags & 0x200 /* sample_size_present */) != 0; - boolean sampleFlagsPresent = (flags & 0x400 /* sample_flags_present */) != 0; + boolean sampleDurationsPresent = (atomFlags & 0x100 /* sample_duration_present */) != 0; + boolean sampleSizesPresent = (atomFlags & 0x200 /* sample_size_present */) != 0; + boolean sampleFlagsPresent = (atomFlags & 0x400 /* sample_flags_present */) != 0; boolean sampleCompositionTimeOffsetsPresent = - (flags & 0x800 /* sample_composition_time_offsets_present */) != 0; + (atomFlags & 0x800 /* sample_composition_time_offsets_present */) != 0; // Offset to the entire video timeline. In the presence of B-frames this is usually used to // ensure that the first frame's presentation timestamp is zero. @@ -551,16 +598,16 @@ public final class FragmentedMp4Extractor implements Extractor { edtsOffset = Util.scaleLargeTimestamp(track.editListMediaTimes[0], 1000, track.timescale); } - out.initTables(sampleCount); - int[] sampleSizeTable = out.sampleSizeTable; - int[] sampleCompositionTimeOffsetTable = out.sampleCompositionTimeOffsetTable; - long[] sampleDecodingTimeTable = out.sampleDecodingTimeTable; - boolean[] sampleIsSyncFrameTable = out.sampleIsSyncFrameTable; + fragment.initTables(sampleCount); + int[] sampleSizeTable = fragment.sampleSizeTable; + int[] sampleCompositionTimeOffsetTable = fragment.sampleCompositionTimeOffsetTable; + long[] sampleDecodingTimeTable = fragment.sampleDecodingTimeTable; + boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable; long timescale = track.timescale; long cumulativeTime = decodeTime; boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_vide - && (workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) != 0; + && (flags & FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) != 0; for (int i = 0; i < sampleCount; i++) { // Use trun values if present, otherwise tfhd, otherwise trex. int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt() @@ -633,7 +680,7 @@ public final class FragmentedMp4Extractor implements Extractor { /** * Parses a sidx atom (defined in 14496-12). */ - private static ChunkIndex parseSidx(long durationUs, ParsableByteArray atom, long inputPosition) + private static ChunkIndex parseSidx(ParsableByteArray atom, long inputPosition) throws ParserException { atom.setPosition(Atom.HEADER_SIZE); int fullAtom = atom.readInt(); @@ -684,17 +731,31 @@ public final class FragmentedMp4Extractor implements Extractor { offset += sizes[i]; } - return new ChunkIndex(durationUs, sizes, offsets, durationsUs, timesUs); + return new ChunkIndex(sizes, offsets, durationsUs, timesUs); } private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException { - int bytesToSkip = (int) (fragmentRun.auxiliaryDataPosition - input.getPosition()); + TrackBundle nextTrackBundle = null; + long nextDataOffset = Long.MAX_VALUE; + int trackBundlesSize = trackBundles.size(); + for (int i = 0; i < trackBundlesSize; i++) { + TrackFragment trackFragment = trackBundles.valueAt(i).fragment; + if (trackFragment.sampleEncryptionDataNeedsFill + && trackFragment.auxiliaryDataPosition < nextDataOffset) { + nextDataOffset = trackFragment.auxiliaryDataPosition; + nextTrackBundle = trackBundles.valueAt(i); + } + } + if (nextTrackBundle == null) { + parserState = STATE_READING_SAMPLE_START; + return; + } + int bytesToSkip = (int) (nextDataOffset - input.getPosition()); if (bytesToSkip < 0) { throw new ParserException("Offset to encryption data was negative."); } input.skipFully(bytesToSkip); - fragmentRun.fillEncryptionData(input); - parserState = STATE_READING_SAMPLE_START; + nextTrackBundle.fragment.fillEncryptionData(input); } /** @@ -713,36 +774,44 @@ public final class FragmentedMp4Extractor implements Extractor { */ private boolean readSample(ExtractorInput input) throws IOException, InterruptedException { if (parserState == STATE_READING_SAMPLE_START) { - if (sampleIndex == fragmentRun.length) { - // We've run out of samples in the current mdat. Discard any trailing data and prepare to - // read the header of the next atom. - int bytesToSkip = (int) (endOfMdatPosition - input.getPosition()); - if (bytesToSkip < 0) { - throw new ParserException("Offset to end of mdat was negative."); + if (currentTrackBundle == null) { + currentTrackBundle = getNextFragmentRun(trackBundles); + if (currentTrackBundle == null) { + // We've run out of samples in the current mdat. Discard any trailing data and prepare to + // read the header of the next atom. + int bytesToSkip = (int) (endOfMdatPosition - input.getPosition()); + if (bytesToSkip < 0) { + throw new ParserException("Offset to end of mdat was negative."); + } + input.skipFully(bytesToSkip); + enterReadingAtomHeaderState(); + return false; } - input.skipFully(bytesToSkip); - enterReadingAtomHeaderState(); - return false; - } - if (sampleIndex == 0) { - // We're reading the first sample in the current mdat. Discard any preceding data. - int bytesToSkip = (int) (fragmentRun.dataPosition - input.getPosition()); + + long nextDataPosition = currentTrackBundle.fragment.dataPosition; + // We skip bytes preceding the next sample to read. + int bytesToSkip = (int) (nextDataPosition - input.getPosition()); if (bytesToSkip < 0) { throw new ParserException("Offset to sample data was negative."); } input.skipFully(bytesToSkip); } - sampleSize = fragmentRun.sampleSizeTable[sampleIndex]; - if (fragmentRun.definesEncryptionData) { - sampleBytesWritten = appendSampleEncryptionData(fragmentRun.sampleEncryptionData); + sampleSize = currentTrackBundle.fragment + .sampleSizeTable[currentTrackBundle.currentSampleIndex]; + if (currentTrackBundle.fragment.definesEncryptionData) { + sampleBytesWritten = appendSampleEncryptionData(currentTrackBundle); sampleSize += sampleBytesWritten; } else { sampleBytesWritten = 0; } - sampleCurrentNalBytesRemaining = 0; parserState = STATE_READING_SAMPLE_CONTINUE; + sampleCurrentNalBytesRemaining = 0; } + TrackFragment fragment = currentTrackBundle.fragment; + Track track = currentTrackBundle.track; + TrackOutput output = currentTrackBundle.output; + int sampleIndex = currentTrackBundle.currentSampleIndex; if (track.nalUnitLengthFieldLength != -1) { // Zero the top three bytes of the array that we'll use to parse nal unit lengths, in case // they're only 1 or 2 bytes long. @@ -763,49 +832,88 @@ public final class FragmentedMp4Extractor implements Extractor { sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); // Write a start code for the current NAL unit. nalStartCode.setPosition(0); - trackOutput.sampleData(nalStartCode, 4); + output.sampleData(nalStartCode, 4); sampleBytesWritten += 4; sampleSize += nalUnitLengthFieldLengthDiff; } else { // Write the payload of the NAL unit. - int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false); + int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } } } else { while (sampleBytesWritten < sampleSize) { - int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); + int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); sampleBytesWritten += writtenBytes; } } - long sampleTimeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L; - int sampleFlags = (fragmentRun.definesEncryptionData ? C.SAMPLE_FLAG_ENCRYPTED : 0) - | (fragmentRun.sampleIsSyncFrameTable[sampleIndex] ? C.SAMPLE_FLAG_SYNC : 0); - int sampleDescriptionIndex = fragmentRun.header.sampleDescriptionIndex; - byte[] encryptionKey = fragmentRun.definesEncryptionData + long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; + int sampleFlags = (fragment.definesEncryptionData ? C.SAMPLE_FLAG_ENCRYPTED : 0) + | (fragment.sampleIsSyncFrameTable[sampleIndex] ? C.SAMPLE_FLAG_SYNC : 0); + int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; + byte[] encryptionKey = fragment.definesEncryptionData ? track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex].keyId : null; - trackOutput.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey); + output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey); - sampleIndex++; + currentTrackBundle.currentSampleIndex++; + if (currentTrackBundle.currentSampleIndex == fragment.length) { + currentTrackBundle = null; + } parserState = STATE_READING_SAMPLE_START; return true; } - private int appendSampleEncryptionData(ParsableByteArray sampleEncryptionData) { - int sampleDescriptionIndex = fragmentRun.header.sampleDescriptionIndex; - TrackEncryptionBox encryptionBox = - track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; + /** + * Returns the {@link TrackBundle} whose fragment run has the earliest file position out of those + * yet to be consumed, or null if all have been consumed. + */ + private static TrackBundle getNextFragmentRun(SparseArray trackBundles) { + TrackBundle nextTrackBundle = null; + long nextTrackRunOffset = Long.MAX_VALUE; + + int trackBundlesSize = trackBundles.size(); + for (int i = 0; i < trackBundlesSize; i++) { + TrackBundle trackBundle = trackBundles.valueAt(i); + if (trackBundle.currentSampleIndex == trackBundle.fragment.length) { + // This track fragment contains no more runs in the next mdat box. + } else { + long trunOffset = trackBundle.fragment.dataPosition; + if (trunOffset < nextTrackRunOffset) { + nextTrackBundle = trackBundle; + nextTrackRunOffset = trunOffset; + } + } + } + return nextTrackBundle; + } + + /** + * Appends the corresponding encryption data to the {@link TrackOutput} contained in the given + * {@link TrackBundle}. + * + * @param trackBundle The {@link TrackBundle} that contains the {@link Track} for which the + * Sample encryption data must be output. + * @return The number of written bytes. + */ + private int appendSampleEncryptionData(TrackBundle trackBundle) { + TrackFragment trackFragment = trackBundle.fragment; + ParsableByteArray sampleEncryptionData = trackFragment.sampleEncryptionData; + int sampleDescriptionIndex = trackFragment.header.sampleDescriptionIndex; + TrackEncryptionBox encryptionBox = trackBundle.track + .sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; int vectorSize = encryptionBox.initializationVectorSize; - boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex]; + boolean subsampleEncryption = trackFragment + .sampleHasSubsampleEncryptionTable[trackBundle.currentSampleIndex]; // Write the signal byte, containing the vector size and the subsample encryption flag. encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0)); encryptionSignalByte.setPosition(0); - trackOutput.sampleData(encryptionSignalByte, 1); + TrackOutput output = trackBundle.output; + output.sampleData(encryptionSignalByte, 1); // Write the vector. - trackOutput.sampleData(sampleEncryptionData, vectorSize); + output.sampleData(sampleEncryptionData, vectorSize); // If we don't have subsample encryption data, we're done. if (!subsampleEncryption) { return 1 + vectorSize; @@ -814,7 +922,7 @@ public final class FragmentedMp4Extractor implements Extractor { int subsampleCount = sampleEncryptionData.readUnsignedShort(); sampleEncryptionData.skipBytes(-2); int subsampleDataLength = 2 + 6 * subsampleCount; - trackOutput.sampleData(sampleEncryptionData, subsampleDataLength); + output.sampleData(sampleEncryptionData, subsampleDataLength); return 1 + vectorSize + subsampleDataLength; } @@ -835,4 +943,25 @@ public final class FragmentedMp4Extractor implements Extractor { || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts; } + /** + * Holds data corresponding to a single track. + */ + private static final class TrackBundle { + + public final Track track; + public final TrackFragment fragment; + public final TrackOutput output; + public final DefaultSampleValues defaultSampleValues; + public int currentSampleIndex; + + public TrackBundle(Track track, TrackFragment fragment, TrackOutput output, + DefaultSampleValues defaultSampleValues) { + this.track = Assertions.checkNotNull(track); + this.fragment = Assertions.checkNotNull(fragment); + this.output = Assertions.checkNotNull(output); + this.defaultSampleValues = Assertions.checkNotNull(defaultSampleValues); + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 0a6facaff0..256ed965dc 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -338,9 +338,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { Track track = new Track(j, streamElementType, timescale, C.UNKNOWN_TIME_US, durationUs, formats[j], trackEncryptionBoxes, nalUnitLengthFieldLength, null, null); FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( - FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME - | FragmentedMp4Extractor.WORKAROUND_IGNORE_TFDT_BOX); - extractor.setTrack(track); + FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME + | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track); extractorWrappers[j] = new ChunkExtractorWrapper(extractor); } elementIndex = i;