From bfa1de68d8528af30d6863c82e059753c0f403d1 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 6 Feb 2015 11:43:37 +0000 Subject: [PATCH] Move common MP4 parsing code to CommonMp4AtomParsers and Mp4Util. Also add parseMp4vFromParent and return the track's duration in parseTrak. This is in preparation for adding a non-fragmented MP4 extractor. --- .../exoplayer/chunk/parser/Extractor.java | 6 + .../parser/mp4/FragmentedMp4Extractor.java | 506 ++---------------- .../chunk/parser/webm/WebmExtractor.java | 6 + .../exoplayer/mp4/CommonMp4AtomParsers.java | 481 +++++++++++++++++ .../google/android/exoplayer/mp4/Mp4Util.java | 93 ++++ .../google/android/exoplayer/mp4/Track.java | 16 +- .../SmoothStreamingChunkSource.java | 4 +- 7 files changed, 632 insertions(+), 480 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java create mode 100644 library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/Extractor.java b/library/src/main/java/com/google/android/exoplayer/chunk/parser/Extractor.java index 3a84099d86..d501e26bcb 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/parser/Extractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.chunk.parser; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleHolder; @@ -78,6 +79,11 @@ public interface Extractor { */ public MediaFormat getFormat(); + /** + * Returns the duration of the stream in microseconds, or {@link C#UNKNOWN_TIME_US} if unknown. + */ + public long getDurationUs(); + /** * Returns the pssh information parsed from the stream. * diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java index d5c7151199..01646ea66f 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java @@ -24,21 +24,18 @@ import com.google.android.exoplayer.chunk.parser.SegmentIndex; import com.google.android.exoplayer.mp4.Atom; import com.google.android.exoplayer.mp4.Atom.ContainerAtom; import com.google.android.exoplayer.mp4.Atom.LeafAtom; +import com.google.android.exoplayer.mp4.CommonMp4AtomParsers; +import com.google.android.exoplayer.mp4.Mp4Util; import com.google.android.exoplayer.mp4.Track; import com.google.android.exoplayer.upstream.NonBlockingInputStream; -import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.CodecSpecificDataUtil; -import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.Util; import android.annotation.SuppressLint; import android.media.MediaCodec; import android.media.MediaExtractor; -import android.util.Pair; import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -67,14 +64,8 @@ public final class FragmentedMp4Extractor implements Extractor { private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM | RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER; - private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; 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}; - /** Channel counts for AC-3 audio, indexed by acmod. (See ETSI TS 102 366.) */ - private static final int[] AC3_CHANNEL_COUNTS = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; - /** Nominal bit-rates for AC-3 audio in kbps, indexed by bit_rate_code. (See ETSI TS 102 366.) */ - private static final int[] AC3_BIT_RATES = new int[] {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, - 192, 224, 256, 320, 384, 448, 512, 576, 640}; // Parser states private static final int STATE_READING_ATOM_HEADER = 0; @@ -82,10 +73,6 @@ public final class FragmentedMp4Extractor implements Extractor { private static final int STATE_READING_ENCRYPTION_DATA = 2; private static final int STATE_READING_SAMPLE = 3; - // Atom data offsets - private static final int ATOM_HEADER_SIZE = 8; - private static final int FULL_ATOM_HEADER_SIZE = 12; - // Atoms that the parser cares about private static final Set PARSED_ATOMS; static { @@ -173,7 +160,7 @@ public final class FragmentedMp4Extractor implements Extractor { public FragmentedMp4Extractor(int workaroundFlags) { this.workaroundFlags = workaroundFlags; parserState = STATE_READING_ATOM_HEADER; - atomHeader = new ParsableByteArray(ATOM_HEADER_SIZE); + atomHeader = new ParsableByteArray(Mp4Util.ATOM_HEADER_SIZE); extendedTypeScratch = new byte[16]; containerAtoms = new Stack(); fragmentRun = new TrackFragment(); @@ -210,6 +197,11 @@ public final class FragmentedMp4Extractor implements Extractor { return track == null ? null : track.mediaFormat; } + @Override + public long getDurationUs() { + return track == null ? C.UNKNOWN_TIME_US : track.durationUs; + } + @Override public int read(NonBlockingInputStream inputStream, SampleHolder out) throws ParserException { @@ -276,14 +268,14 @@ public final class FragmentedMp4Extractor implements Extractor { } private int readAtomHeader(NonBlockingInputStream inputStream) { - int remainingBytes = ATOM_HEADER_SIZE - atomBytesRead; + int remainingBytes = Mp4Util.ATOM_HEADER_SIZE - atomBytesRead; int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes); if (bytesRead == -1) { return RESULT_END_OF_STREAM; } rootAtomBytesRead += bytesRead; atomBytesRead += bytesRead; - if (atomBytesRead != ATOM_HEADER_SIZE) { + if (atomBytesRead != Mp4Util.ATOM_HEADER_SIZE) { return RESULT_NEED_MORE_DATA; } @@ -305,10 +297,10 @@ public final class FragmentedMp4Extractor implements Extractor { if (CONTAINER_TYPES.contains(atomTypeInteger)) { enterState(STATE_READING_ATOM_HEADER); containerAtoms.add(new ContainerAtom(atomType, - rootAtomBytesRead + atomSize - ATOM_HEADER_SIZE)); + rootAtomBytesRead + atomSize - Mp4Util.ATOM_HEADER_SIZE)); } else { atomData = new ParsableByteArray(atomSize); - System.arraycopy(atomHeader.data, 0, atomData.data, 0, ATOM_HEADER_SIZE); + System.arraycopy(atomHeader.data, 0, atomData.data, 0, Mp4Util.ATOM_HEADER_SIZE); enterState(STATE_READING_ATOM_PAYLOAD); } } else { @@ -377,7 +369,7 @@ public final class FragmentedMp4Extractor implements Extractor { LeafAtom child = moovChildren.get(i); if (child.type == Atom.TYPE_pssh) { ParsableByteArray psshAtom = child.data; - psshAtom.setPosition(FULL_ATOM_HEADER_SIZE); + psshAtom.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE); UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong()); int dataSize = psshAtom.readInt(); byte[] data = new byte[dataSize]; @@ -387,7 +379,7 @@ public final class FragmentedMp4Extractor implements Extractor { } ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).data); - track = parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak)); + track = CommonMp4AtomParsers.parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak)); } private void onMoofContainerAtomRead(ContainerAtom moof) { @@ -412,7 +404,7 @@ public final class FragmentedMp4Extractor implements Extractor { * Parses a trex atom (defined in 14496-12). */ private static DefaultSampleValues parseTrex(ParsableByteArray trex) { - trex.setPosition(FULL_ATOM_HEADER_SIZE + 4); + trex.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE + 4); int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1; int defaultSampleDuration = trex.readUnsignedIntToInt(); int defaultSampleSize = trex.readUnsignedIntToInt(); @@ -421,388 +413,6 @@ public final class FragmentedMp4Extractor implements Extractor { defaultSampleSize, defaultSampleFlags); } - /** - * Parses a trak atom (defined in 14496-12). - */ - private static Track parseTrak(ContainerAtom trak) { - ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); - int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data); - Assertions.checkState(trackType == Track.TYPE_AUDIO || trackType == Track.TYPE_VIDEO - || trackType == Track.TYPE_TEXT); - - Pair header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data); - int id = header.first; - // TODO: This value should be used to set a duration field on the Track object - // instantiated below, however we've found examples where the value is 0. Revisit whether we - // should set it anyway (and just have it be wrong for bad media streams). - // long duration = header.second; - long timescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); - ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf) - .getContainerAtomOfType(Atom.TYPE_stbl); - - Pair sampleDescriptions = - parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data); - return new Track(id, trackType, timescale, sampleDescriptions.first, sampleDescriptions.second); - } - - /** - * Parses a tkhd atom (defined in 14496-12). - * - * @return A {@link Pair} consisting of the track id and duration (in the timescale indicated in - * the movie header box). The duration is set to -1 if the duration is unspecified. - */ - private static Pair parseTkhd(ParsableByteArray tkhd) { - tkhd.setPosition(ATOM_HEADER_SIZE); - int fullAtom = tkhd.readInt(); - int version = parseFullAtomVersion(fullAtom); - - tkhd.skip(version == 0 ? 8 : 16); - - int trackId = tkhd.readInt(); - tkhd.skip(4); - - boolean durationUnknown = true; - int durationPosition = tkhd.getPosition(); - int durationByteCount = version == 0 ? 4 : 8; - for (int i = 0; i < durationByteCount; i++) { - if (tkhd.data[durationPosition + i] != -1) { - durationUnknown = false; - break; - } - } - long duration; - if (durationUnknown) { - tkhd.skip(durationByteCount); - duration = -1; - } else { - duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong(); - } - - return Pair.create(trackId, duration); - } - - /** - * Parses an hdlr atom (defined in 14496-12). - * - * @param hdlr The hdlr atom to parse. - * @return The track type. - */ - private static int parseHdlr(ParsableByteArray hdlr) { - hdlr.setPosition(FULL_ATOM_HEADER_SIZE + 4); - return hdlr.readInt(); - } - - /** - * Parses an mdhd atom (defined in 14496-12). - * - * @param mdhd The mdhd atom to parse. - * @return The media timescale, defined as the number of time units that pass in one second. - */ - private static long parseMdhd(ParsableByteArray mdhd) { - mdhd.setPosition(ATOM_HEADER_SIZE); - int fullAtom = mdhd.readInt(); - int version = parseFullAtomVersion(fullAtom); - - mdhd.skip(version == 0 ? 8 : 16); - return mdhd.readUnsignedInt(); - } - - private static Pair parseStsd(ParsableByteArray stsd) { - stsd.setPosition(FULL_ATOM_HEADER_SIZE); - int numberOfEntries = stsd.readInt(); - MediaFormat mediaFormat = null; - TrackEncryptionBox[] trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries]; - for (int i = 0; i < numberOfEntries; i++) { - int childStartPosition = stsd.getPosition(); - int childAtomSize = stsd.readInt(); - int childAtomType = stsd.readInt(); - if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 - || childAtomType == Atom.TYPE_encv) { - Pair avc = - parseAvcFromParent(stsd, childStartPosition, childAtomSize); - mediaFormat = avc.first; - trackEncryptionBoxes[i] = avc.second; - } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca - || childAtomType == Atom.TYPE_ac_3) { - Pair audioSampleEntry = - parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize); - mediaFormat = audioSampleEntry.first; - trackEncryptionBoxes[i] = audioSampleEntry.second; - } else if (childAtomType == Atom.TYPE_TTML) { - mediaFormat = MediaFormat.createTtmlFormat(); - } - stsd.setPosition(childStartPosition + childAtomSize); - } - return Pair.create(mediaFormat, trackEncryptionBoxes); - } - - private static Pair parseAvcFromParent(ParsableByteArray parent, - int position, int size) { - parent.setPosition(position + ATOM_HEADER_SIZE); - - parent.skip(24); - int width = parent.readUnsignedShort(); - int height = parent.readUnsignedShort(); - float pixelWidthHeightRatio = 1; - parent.skip(50); - - List initializationData = null; - TrackEncryptionBox trackEncryptionBox = null; - int childPosition = parent.getPosition(); - while (childPosition - position < size) { - parent.setPosition(childPosition); - int childStartPosition = parent.getPosition(); - int childAtomSize = parent.readInt(); - int childAtomType = parent.readInt(); - if (childAtomType == Atom.TYPE_avcC) { - initializationData = parseAvcCFromParent(parent, childStartPosition); - } else if (childAtomType == Atom.TYPE_sinf) { - trackEncryptionBox = parseSinfFromParent(parent, childStartPosition, childAtomSize); - } else if (childAtomType == Atom.TYPE_pasp) { - pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition); - } - childPosition += childAtomSize; - } - - MediaFormat format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, - width, height, pixelWidthHeightRatio, initializationData); - return Pair.create(format, trackEncryptionBox); - } - - private static Pair parseAudioSampleEntry( - ParsableByteArray parent, int atomType, int position, int size) { - parent.setPosition(position + ATOM_HEADER_SIZE); - parent.skip(16); - int channelCount = parent.readUnsignedShort(); - int sampleSize = parent.readUnsignedShort(); - parent.skip(4); - int sampleRate = parent.readUnsignedFixedPoint1616(); - int bitrate = MediaFormat.NO_VALUE; - - byte[] initializationData = null; - TrackEncryptionBox trackEncryptionBox = null; - int childPosition = parent.getPosition(); - while (childPosition - position < size) { - parent.setPosition(childPosition); - int childStartPosition = parent.getPosition(); - int childAtomSize = parent.readInt(); - int childAtomType = parent.readInt(); - if (atomType == Atom.TYPE_mp4a || atomType == Atom.TYPE_enca) { - if (childAtomType == Atom.TYPE_esds) { - initializationData = parseEsdsFromParent(parent, childStartPosition); - // TODO: Do we really need to do this? See [Internal: b/10903778] - // Update sampleRate and channelCount from the AudioSpecificConfig initialization data. - Pair audioSpecificConfig = - CodecSpecificDataUtil.parseAudioSpecificConfig(initializationData); - sampleRate = audioSpecificConfig.first; - channelCount = audioSpecificConfig.second; - } else if (childAtomType == Atom.TYPE_sinf) { - trackEncryptionBox = parseSinfFromParent(parent, childStartPosition, childAtomSize); - } - } else if (atomType == Atom.TYPE_ac_3 && childAtomType == Atom.TYPE_dac3) { - // TODO: Choose the right AC-3 track based on the contents of dac3/dec3. - Ac3Format ac3Format = - parseAc3SpecificBoxFromParent(parent, childStartPosition); - if (ac3Format != null) { - sampleRate = ac3Format.sampleRate; - channelCount = ac3Format.channelCount; - bitrate = ac3Format.bitrate; - } - - // TODO: Add support for encrypted AC-3. - trackEncryptionBox = null; - } else if (atomType == Atom.TYPE_ec_3 && childAtomType == Atom.TYPE_dec3) { - sampleRate = parseEc3SpecificBoxFromParent(parent, childStartPosition); - trackEncryptionBox = null; - } - childPosition += childAtomSize; - } - - String mimeType; - if (atomType == Atom.TYPE_ac_3) { - mimeType = MimeTypes.AUDIO_AC3; - } else if (atomType == Atom.TYPE_ec_3) { - mimeType = MimeTypes.AUDIO_EC3; - } else { - mimeType = MimeTypes.AUDIO_AAC; - } - - MediaFormat format = MediaFormat.createAudioFormat( - mimeType, sampleSize, channelCount, sampleRate, bitrate, - initializationData == null ? null : Collections.singletonList(initializationData)); - return Pair.create(format, trackEncryptionBox); - } - - private static Ac3Format parseAc3SpecificBoxFromParent(ParsableByteArray parent, int position) { - // Start of the dac3 atom (defined in ETSI TS 102 366) - parent.setPosition(position + ATOM_HEADER_SIZE); - - // fscod (sample rate code) - int fscod = (parent.readUnsignedByte() & 0xC0) >> 6; - int sampleRate; - switch (fscod) { - case 0: - sampleRate = 48000; - break; - case 1: - sampleRate = 44100; - break; - case 2: - sampleRate = 32000; - break; - default: - // TODO: The decoder should not use this stream. - return null; - } - - int nextByte = parent.readUnsignedByte(); - - // Map acmod (audio coding mode) onto a channel count. - int channelCount = AC3_CHANNEL_COUNTS[(nextByte & 0x38) >> 3]; - - // lfeon (low frequency effects on) - if ((nextByte & 0x04) != 0) { - channelCount++; - } - - // Map bit_rate_code onto a bit-rate in kbit/s. - int bitrate = AC3_BIT_RATES[((nextByte & 0x03) << 3) + (parent.readUnsignedByte() >> 5)]; - - return new Ac3Format(channelCount, sampleRate, bitrate); - } - - private static int parseEc3SpecificBoxFromParent(ParsableByteArray parent, int position) { - // Start of the dec3 atom (defined in ETSI TS 102 366) - parent.setPosition(position + ATOM_HEADER_SIZE); - // TODO: Implement parsing for enhanced AC-3 with multiple sub-streams. - return 0; - } - - private static List parseAvcCFromParent(ParsableByteArray parent, int position) { - parent.setPosition(position + ATOM_HEADER_SIZE + 4); - // Start of the AVCDecoderConfigurationRecord (defined in 14496-15) - int nalUnitLength = (parent.readUnsignedByte() & 0x3) + 1; - if (nalUnitLength != 4) { - // readSample currently relies on a nalUnitLength of 4. - // TODO: Consider handling the case where it isn't. - throw new IllegalStateException(); - } - List initializationData = new ArrayList(); - // TODO: We should try and parse these using CodecSpecificDataUtil.parseSpsNalUnit, and - // expose the AVC profile and level somewhere useful; Most likely in MediaFormat. - int numSequenceParameterSets = parent.readUnsignedByte() & 0x1F; - for (int j = 0; j < numSequenceParameterSets; j++) { - initializationData.add(parseChildNalUnit(parent)); - } - int numPictureParamterSets = parent.readUnsignedByte(); - for (int j = 0; j < numPictureParamterSets; j++) { - initializationData.add(parseChildNalUnit(parent)); - } - return initializationData; - } - - private static byte[] parseChildNalUnit(ParsableByteArray atom) { - int length = atom.readUnsignedShort(); - int offset = atom.getPosition(); - atom.skip(length); - return CodecSpecificDataUtil.buildNalUnit(atom.data, offset, length); - } - - private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position, - int size) { - int childPosition = position + ATOM_HEADER_SIZE; - - TrackEncryptionBox trackEncryptionBox = null; - while (childPosition - position < size) { - parent.setPosition(childPosition); - int childAtomSize = parent.readInt(); - int childAtomType = parent.readInt(); - if (childAtomType == Atom.TYPE_frma) { - parent.readInt(); // dataFormat. - } else if (childAtomType == Atom.TYPE_schm) { - parent.skip(4); - parent.readInt(); // schemeType. Expect cenc - parent.readInt(); // schemeVersion. Expect 0x00010000 - } else if (childAtomType == Atom.TYPE_schi) { - trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize); - } - childPosition += childAtomSize; - } - - return trackEncryptionBox; - } - - private static float parsePaspFromParent(ParsableByteArray parent, int position) { - parent.setPosition(position + ATOM_HEADER_SIZE); - int hSpacing = parent.readUnsignedIntToInt(); - int vSpacing = parent.readUnsignedIntToInt(); - return (float) hSpacing / vSpacing; - } - - private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, - int size) { - int childPosition = position + ATOM_HEADER_SIZE; - while (childPosition - position < size) { - parent.setPosition(childPosition); - int childAtomSize = parent.readInt(); - int childAtomType = parent.readInt(); - if (childAtomType == Atom.TYPE_tenc) { - parent.skip(4); - int firstInt = parent.readInt(); - boolean defaultIsEncrypted = (firstInt >> 8) == 1; - int defaultInitVectorSize = firstInt & 0xFF; - byte[] defaultKeyId = new byte[16]; - parent.readBytes(defaultKeyId, 0, defaultKeyId.length); - return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId); - } - childPosition += childAtomSize; - } - return null; - } - - private static byte[] parseEsdsFromParent(ParsableByteArray parent, int position) { - parent.setPosition(position + ATOM_HEADER_SIZE + 4); - // Start of the ES_Descriptor (defined in 14496-1) - parent.skip(1); // ES_Descriptor tag - int varIntByte = parent.readUnsignedByte(); - while (varIntByte > 127) { - varIntByte = parent.readUnsignedByte(); - } - parent.skip(2); // ES_ID - - int flags = parent.readUnsignedByte(); - if ((flags & 0x80 /* streamDependenceFlag */) != 0) { - parent.skip(2); - } - if ((flags & 0x40 /* URL_Flag */) != 0) { - parent.skip(parent.readUnsignedShort()); - } - if ((flags & 0x20 /* OCRstreamFlag */) != 0) { - parent.skip(2); - } - - // Start of the DecoderConfigDescriptor (defined in 14496-1) - parent.skip(1); // DecoderConfigDescriptor tag - varIntByte = parent.readUnsignedByte(); - while (varIntByte > 127) { - varIntByte = parent.readUnsignedByte(); - } - parent.skip(13); - - // Start of AudioSpecificConfig (defined in 14496-3) - parent.skip(1); // AudioSpecificConfig tag - varIntByte = parent.readUnsignedByte(); - int varInt = varIntByte & 0x7F; - while (varIntByte > 127) { - varIntByte = parent.readUnsignedByte(); - varInt = varInt << 8; - varInt |= varIntByte & 0x7F; - } - byte[] initializationData = new byte[varInt]; - parent.readBytes(initializationData, 0, varInt); - return initializationData; - } - private static void parseMoof(Track track, DefaultSampleValues extendsDefaults, ContainerAtom moof, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) { parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf), @@ -848,9 +458,9 @@ public final class FragmentedMp4Extractor implements Extractor { private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz, TrackFragment out) { int vectorSize = encryptionBox.initializationVectorSize; - saiz.setPosition(ATOM_HEADER_SIZE); + saiz.setPosition(Mp4Util.ATOM_HEADER_SIZE); int fullAtom = saiz.readInt(); - int flags = parseFullAtomFlags(fullAtom); + int flags = Mp4Util.parseFullAtomFlags(fullAtom); if ((flags & 0x01) == 1) { saiz.skip(8); } @@ -885,9 +495,9 @@ public final class FragmentedMp4Extractor implements Extractor { */ private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults, ParsableByteArray tfhd) { - tfhd.setPosition(ATOM_HEADER_SIZE); + tfhd.setPosition(Mp4Util.ATOM_HEADER_SIZE); int fullAtom = tfhd.readInt(); - int flags = parseFullAtomFlags(fullAtom); + int flags = Mp4Util.parseFullAtomFlags(fullAtom); tfhd.skip(4); // trackId if ((flags & 0x01 /* base_data_offset_present */) != 0) { @@ -914,9 +524,9 @@ public final class FragmentedMp4Extractor implements Extractor { * media, expressed in the media's timescale. */ private static long parseTfdt(ParsableByteArray tfdt) { - tfdt.setPosition(ATOM_HEADER_SIZE); + tfdt.setPosition(Mp4Util.ATOM_HEADER_SIZE); int fullAtom = tfdt.readInt(); - int version = parseFullAtomVersion(fullAtom); + int version = Mp4Util.parseFullAtomVersion(fullAtom); return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt(); } @@ -931,9 +541,9 @@ public final class FragmentedMp4Extractor implements Extractor { */ private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues, long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) { - trun.setPosition(ATOM_HEADER_SIZE); + trun.setPosition(Mp4Util.ATOM_HEADER_SIZE); int fullAtom = trun.readInt(); - int flags = parseFullAtomFlags(fullAtom); + int flags = Mp4Util.parseFullAtomFlags(fullAtom); int sampleCount = trun.readUnsignedIntToInt(); if ((flags & 0x01 /* data_offset_present */) != 0) { @@ -991,7 +601,7 @@ public final class FragmentedMp4Extractor implements Extractor { private static void parseUuid(ParsableByteArray uuid, TrackFragment out, byte[] extendedTypeScratch) { - uuid.setPosition(ATOM_HEADER_SIZE); + uuid.setPosition(Mp4Util.ATOM_HEADER_SIZE); uuid.readBytes(extendedTypeScratch, 0, 16); // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox. @@ -1010,9 +620,9 @@ public final class FragmentedMp4Extractor implements Extractor { } private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) { - senc.setPosition(ATOM_HEADER_SIZE + offset); + senc.setPosition(Mp4Util.ATOM_HEADER_SIZE + offset); int fullAtom = senc.readInt(); - int flags = parseFullAtomFlags(fullAtom); + int flags = Mp4Util.parseFullAtomFlags(fullAtom); if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) { // TODO: Implement this. @@ -1034,9 +644,9 @@ public final class FragmentedMp4Extractor implements Extractor { * Parses a sidx atom (defined in 14496-12). */ private static SegmentIndex parseSidx(ParsableByteArray atom) { - atom.setPosition(ATOM_HEADER_SIZE); + atom.setPosition(Mp4Util.ATOM_HEADER_SIZE); int fullAtom = atom.readInt(); - int version = parseFullAtomVersion(fullAtom); + int version = Mp4Util.parseFullAtomVersion(fullAtom); atom.skip(4); long timescale = atom.readUnsignedInt(); @@ -1176,17 +786,8 @@ public final class FragmentedMp4Extractor implements Extractor { inputStream.read(outputData, sampleSize); if (track.type == Track.TYPE_VIDEO) { // The mp4 file contains length-prefixed NAL units, but the decoder wants start code - // delimited content. Replace length prefixes with start codes. - int sampleOffset = outputData.position() - sampleSize; - int position = sampleOffset; - while (position < sampleOffset + sampleSize) { - outputData.position(position); - int length = readUnsignedIntToInt(outputData); - outputData.position(position); - outputData.put(NAL_START_CODE); - position += length + 4; - } - outputData.position(sampleOffset + sampleSize); + // delimited content. + Mp4Util.replaceLengthPrefixesWithAvcStartCodes(outputData, sampleSize); } out.size = sampleSize; } @@ -1236,51 +837,4 @@ public final class FragmentedMp4Extractor implements Extractor { } } - /** - * Parses the version number out of the additional integer component of a full atom. - */ - private static int parseFullAtomVersion(int fullAtomInt) { - return 0x000000FF & (fullAtomInt >> 24); - } - - /** - * Parses the atom flags out of the additional integer component of a full atom. - */ - private static int parseFullAtomFlags(int fullAtomInt) { - return 0x00FFFFFF & fullAtomInt; - } - - /** - * Reads an unsigned integer into an integer. This method is suitable for use when it can be - * assumed that the top bit will always be set to zero. - * - * @throws IllegalArgumentException If the top bit of the input data is set. - */ - private static int readUnsignedIntToInt(ByteBuffer data) { - int result = 0xFF & data.get(); - for (int i = 1; i < 4; i++) { - result <<= 8; - result |= 0xFF & data.get(); - } - if (result < 0) { - throw new IllegalArgumentException("Top bit not zero: " + result); - } - return result; - } - - /** Represents the format for AC-3 audio. */ - private static final class Ac3Format { - - public final int channelCount; - public final int sampleRate; - public final int bitrate; - - public Ac3Format(int channelCount, int sampleRate, int bitrate) { - this.channelCount = channelCount; - this.sampleRate = sampleRate; - this.bitrate = bitrate; - } - - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java index 5d23ae8eb1..829d604a77 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.chunk.parser.webm; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleHolder; @@ -184,6 +185,11 @@ public final class WebmExtractor implements Extractor { return format; } + @Override + public long getDurationUs() { + return durationUs == UNKNOWN ? C.UNKNOWN_TIME_US : durationUs; + } + @Override public Map getPsshInfo() { // TODO: Parse pssh data from Webm streams. diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java new file mode 100644 index 0000000000..dc7e580ca5 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java @@ -0,0 +1,481 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.mp4; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.CodecSpecificDataUtil; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableByteArray; +import com.google.android.exoplayer.util.Util; + +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */ +public final class CommonMp4AtomParsers { + + /** Channel counts for AC-3 audio, indexed by acmod. (See ETSI TS 102 366.) */ + private static final int[] AC3_CHANNEL_COUNTS = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; + /** Nominal bit-rates for AC-3 audio in kbps, indexed by bit_rate_code. (See ETSI TS 102 366.) */ + private static final int[] AC3_BIT_RATES = new int[] {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, + 192, 224, 256, 320, 384, 448, 512, 576, 640}; + + /** + * Parses a trak atom (defined in 14496-12). + * + * @return A {@link Track} instance. + */ + public static Track parseTrak(Atom.ContainerAtom trak) { + Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); + int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data); + Assertions.checkState(trackType == Track.TYPE_AUDIO || trackType == Track.TYPE_VIDEO + || trackType == Track.TYPE_TEXT || trackType == Track.TYPE_TIME_CODE); + + Pair header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data); + int id = header.first; + long duration = header.second; + long timescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); + long durationUs; + if (duration == -1) { + durationUs = C.UNKNOWN_TIME_US; + } else { + durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale); + } + Atom.ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf) + .getContainerAtomOfType(Atom.TYPE_stbl); + + Pair sampleDescriptions = + parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data); + return new Track(id, trackType, timescale, durationUs, sampleDescriptions.first, + sampleDescriptions.second); + } + + /** + * Parses a tkhd atom (defined in 14496-12). + * + * @return A {@link Pair} consisting of the track id and duration (in the timescale indicated in + * the movie header box). The duration is set to -1 if the duration is unspecified. + */ + private static Pair parseTkhd(ParsableByteArray tkhd) { + tkhd.setPosition(Mp4Util.ATOM_HEADER_SIZE); + int fullAtom = tkhd.readInt(); + int version = Mp4Util.parseFullAtomVersion(fullAtom); + + tkhd.skip(version == 0 ? 8 : 16); + + int trackId = tkhd.readInt(); + tkhd.skip(4); + + boolean durationUnknown = true; + int durationPosition = tkhd.getPosition(); + int durationByteCount = version == 0 ? 4 : 8; + for (int i = 0; i < durationByteCount; i++) { + if (tkhd.data[durationPosition + i] != -1) { + durationUnknown = false; + break; + } + } + long duration; + if (durationUnknown) { + tkhd.skip(durationByteCount); + duration = -1; + } else { + duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong(); + } + + return Pair.create(trackId, duration); + } + + /** + * Parses an hdlr atom. + * + * @param hdlr The hdlr atom to parse. + * @return The track type. + */ + private static int parseHdlr(ParsableByteArray hdlr) { + hdlr.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE + 4); + return hdlr.readInt(); + } + + /** + * Parses an mdhd atom (defined in 14496-12). + * + * @param mdhd The mdhd atom to parse. + * @return The media timescale, defined as the number of time units that pass in one second. + */ + private static long parseMdhd(ParsableByteArray mdhd) { + mdhd.setPosition(Mp4Util.ATOM_HEADER_SIZE); + int fullAtom = mdhd.readInt(); + int version = Mp4Util.parseFullAtomVersion(fullAtom); + + mdhd.skip(version == 0 ? 8 : 16); + return mdhd.readUnsignedInt(); + } + + private static Pair parseStsd(ParsableByteArray stsd) { + stsd.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE); + int numberOfEntries = stsd.readInt(); + MediaFormat mediaFormat = null; + TrackEncryptionBox[] trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries]; + for (int i = 0; i < numberOfEntries; i++) { + int childStartPosition = stsd.getPosition(); + int childAtomSize = stsd.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = stsd.readInt(); + if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 + || childAtomType == Atom.TYPE_encv) { + Pair avc = + parseAvcFromParent(stsd, childStartPosition, childAtomSize); + mediaFormat = avc.first; + trackEncryptionBoxes[i] = avc.second; + } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca + || childAtomType == Atom.TYPE_ac_3) { + Pair audioSampleEntry = + parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize); + mediaFormat = audioSampleEntry.first; + trackEncryptionBoxes[i] = audioSampleEntry.second; + } else if (childAtomType == Atom.TYPE_TTML) { + mediaFormat = MediaFormat.createTtmlFormat(); + } else if (childAtomType == Atom.TYPE_mp4v) { + mediaFormat = parseMp4vFromParent(stsd, childStartPosition, childAtomSize); + } + stsd.setPosition(childStartPosition + childAtomSize); + } + return Pair.create(mediaFormat, trackEncryptionBoxes); + } + + /** Returns the media format for an avc1 box. */ + private static Pair parseAvcFromParent(ParsableByteArray parent, + int position, int size) { + parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE); + + parent.skip(24); + int width = parent.readUnsignedShort(); + int height = parent.readUnsignedShort(); + float pixelWidthHeightRatio = 1; + parent.skip(50); + + List initializationData = null; + TrackEncryptionBox trackEncryptionBox = null; + int childPosition = parent.getPosition(); + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childStartPosition = parent.getPosition(); + int childAtomSize = parent.readInt(); + if (childAtomSize == 0 && parent.getPosition() - position == size) { + // Handle optional terminating four zero bytes in MOV files. + break; + } + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_avcC) { + initializationData = parseAvcCFromParent(parent, childStartPosition); + } else if (childAtomType == Atom.TYPE_sinf) { + trackEncryptionBox = parseSinfFromParent(parent, childStartPosition, childAtomSize); + } else if (childAtomType == Atom.TYPE_pasp) { + pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition); + } + childPosition += childAtomSize; + } + + MediaFormat format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, + width, height, pixelWidthHeightRatio, initializationData); + return Pair.create(format, trackEncryptionBox); + } + + private static List parseAvcCFromParent(ParsableByteArray parent, int position) { + parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE + 4); + // Start of the AVCDecoderConfigurationRecord (defined in 14496-15) + int nalUnitLength = (parent.readUnsignedByte() & 0x3) + 1; + if (nalUnitLength != 4) { + // readSample currently relies on a nalUnitLength of 4. + // TODO: Consider handling the case where it isn't. + throw new IllegalStateException(); + } + List initializationData = new ArrayList(); + // TODO: We should try and parse these using CodecSpecificDataUtil.parseSpsNalUnit, and + // expose the AVC profile and level somewhere useful; Most likely in MediaFormat. + int numSequenceParameterSets = parent.readUnsignedByte() & 0x1F; + for (int j = 0; j < numSequenceParameterSets; j++) { + initializationData.add(Mp4Util.parseChildNalUnit(parent)); + } + int numPictureParameterSets = parent.readUnsignedByte(); + for (int j = 0; j < numPictureParameterSets; j++) { + initializationData.add(Mp4Util.parseChildNalUnit(parent)); + } + return initializationData; + } + + private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position, + int size) { + int childPosition = position + Mp4Util.ATOM_HEADER_SIZE; + + TrackEncryptionBox trackEncryptionBox = null; + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childAtomSize = parent.readInt(); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_frma) { + parent.readInt(); // dataFormat. + } else if (childAtomType == Atom.TYPE_schm) { + parent.skip(4); + parent.readInt(); // schemeType. Expect cenc + parent.readInt(); // schemeVersion. Expect 0x00010000 + } else if (childAtomType == Atom.TYPE_schi) { + trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize); + } + childPosition += childAtomSize; + } + + return trackEncryptionBox; + } + + private static float parsePaspFromParent(ParsableByteArray parent, int position) { + parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE); + int hSpacing = parent.readUnsignedIntToInt(); + int vSpacing = parent.readUnsignedIntToInt(); + return (float) hSpacing / vSpacing; + } + + private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, + int size) { + int childPosition = position + Mp4Util.ATOM_HEADER_SIZE; + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childAtomSize = parent.readInt(); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_tenc) { + parent.skip(4); + int firstInt = parent.readInt(); + boolean defaultIsEncrypted = (firstInt >> 8) == 1; + int defaultInitVectorSize = firstInt & 0xFF; + byte[] defaultKeyId = new byte[16]; + parent.readBytes(defaultKeyId, 0, defaultKeyId.length); + return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId); + } + childPosition += childAtomSize; + } + return null; + } + + /** Returns the media format for an mp4v box. */ + private static MediaFormat parseMp4vFromParent(ParsableByteArray parent, + int position, int size) { + parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE); + + parent.skip(24); + int width = parent.readUnsignedShort(); + int height = parent.readUnsignedShort(); + parent.skip(50); + + List initializationData = new ArrayList(1); + int childPosition = parent.getPosition(); + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childStartPosition = parent.getPosition(); + int childAtomSize = parent.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_esds) { + initializationData.add(parseEsdsFromParent(parent, childStartPosition)); + } + childPosition += childAtomSize; + } + + return MediaFormat.createVideoFormat( + MimeTypes.VIDEO_MP4V, MediaFormat.NO_VALUE, width, height, initializationData); + } + + private static Pair parseAudioSampleEntry( + ParsableByteArray parent, int atomType, int position, int size) { + parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE); + parent.skip(16); + int channelCount = parent.readUnsignedShort(); + int sampleSize = parent.readUnsignedShort(); + parent.skip(4); + int sampleRate = parent.readUnsignedFixedPoint1616(); + int bitrate = MediaFormat.NO_VALUE; + + byte[] initializationData = null; + TrackEncryptionBox trackEncryptionBox = null; + int childPosition = parent.getPosition(); + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childStartPosition = parent.getPosition(); + int childAtomSize = parent.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = parent.readInt(); + if (atomType == Atom.TYPE_mp4a || atomType == Atom.TYPE_enca) { + if (childAtomType == Atom.TYPE_esds) { + initializationData = parseEsdsFromParent(parent, childStartPosition); + // TODO: Do we really need to do this? See [Internal: b/10903778] + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data. + Pair audioSpecificConfig = + CodecSpecificDataUtil.parseAudioSpecificConfig(initializationData); + sampleRate = audioSpecificConfig.first; + channelCount = audioSpecificConfig.second; + } else if (childAtomType == Atom.TYPE_sinf) { + trackEncryptionBox = parseSinfFromParent(parent, childStartPosition, childAtomSize); + } + } else if (atomType == Atom.TYPE_ac_3 && childAtomType == Atom.TYPE_dac3) { + // TODO: Choose the right AC-3 track based on the contents of dac3/dec3. + Ac3Format ac3Format = + parseAc3SpecificBoxFromParent(parent, childStartPosition); + if (ac3Format != null) { + sampleRate = ac3Format.sampleRate; + channelCount = ac3Format.channelCount; + bitrate = ac3Format.bitrate; + } + + // TODO: Add support for encrypted AC-3. + trackEncryptionBox = null; + } else if (atomType == Atom.TYPE_ec_3 && childAtomType == Atom.TYPE_dec3) { + sampleRate = parseEc3SpecificBoxFromParent(parent, childStartPosition); + trackEncryptionBox = null; + } + childPosition += childAtomSize; + } + + String mimeType; + if (atomType == Atom.TYPE_ac_3) { + mimeType = MimeTypes.AUDIO_AC3; + } else if (atomType == Atom.TYPE_ec_3) { + mimeType = MimeTypes.AUDIO_EC3; + } else { + mimeType = MimeTypes.AUDIO_AAC; + } + + MediaFormat format = MediaFormat.createAudioFormat( + mimeType, sampleSize, channelCount, sampleRate, bitrate, + initializationData == null ? null : Collections.singletonList(initializationData)); + return Pair.create(format, trackEncryptionBox); + } + + /** Returns codec-specific initialization data contained in an esds box. */ + private static byte[] parseEsdsFromParent(ParsableByteArray parent, int position) { + parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE + 4); + // Start of the ES_Descriptor (defined in 14496-1) + parent.skip(1); // ES_Descriptor tag + int varIntByte = parent.readUnsignedByte(); + while (varIntByte > 127) { + varIntByte = parent.readUnsignedByte(); + } + parent.skip(2); // ES_ID + + int flags = parent.readUnsignedByte(); + if ((flags & 0x80 /* streamDependenceFlag */) != 0) { + parent.skip(2); + } + if ((flags & 0x40 /* URL_Flag */) != 0) { + parent.skip(parent.readUnsignedShort()); + } + if ((flags & 0x20 /* OCRstreamFlag */) != 0) { + parent.skip(2); + } + + // Start of the DecoderConfigDescriptor (defined in 14496-1) + parent.skip(1); // DecoderConfigDescriptor tag + varIntByte = parent.readUnsignedByte(); + while (varIntByte > 127) { + varIntByte = parent.readUnsignedByte(); + } + parent.skip(13); + + // Start of AudioSpecificConfig (defined in 14496-3) + parent.skip(1); // AudioSpecificConfig tag + varIntByte = parent.readUnsignedByte(); + int varInt = varIntByte & 0x7F; + while (varIntByte > 127) { + varIntByte = parent.readUnsignedByte(); + varInt = varInt << 8; + varInt |= varIntByte & 0x7F; + } + byte[] initializationData = new byte[varInt]; + parent.readBytes(initializationData, 0, varInt); + return initializationData; + } + + private static Ac3Format parseAc3SpecificBoxFromParent(ParsableByteArray parent, int position) { + // Start of the dac3 atom (defined in ETSI TS 102 366) + parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE); + + // fscod (sample rate code) + int fscod = (parent.readUnsignedByte() & 0xC0) >> 6; + int sampleRate; + switch (fscod) { + case 0: + sampleRate = 48000; + break; + case 1: + sampleRate = 44100; + break; + case 2: + sampleRate = 32000; + break; + default: + // TODO: The decoder should not use this stream. + return null; + } + + int nextByte = parent.readUnsignedByte(); + + // Map acmod (audio coding mode) onto a channel count. + int channelCount = AC3_CHANNEL_COUNTS[(nextByte & 0x38) >> 3]; + + // lfeon (low frequency effects on) + if ((nextByte & 0x04) != 0) { + channelCount++; + } + + // Map bit_rate_code onto a bit-rate in kbit/s. + int bitrate = AC3_BIT_RATES[((nextByte & 0x03) << 3) + (parent.readUnsignedByte() >> 5)]; + + return new Ac3Format(channelCount, sampleRate, bitrate); + } + + private static int parseEc3SpecificBoxFromParent(ParsableByteArray parent, int position) { + // Start of the dec3 atom (defined in ETSI TS 102 366) + parent.setPosition(position + Mp4Util.ATOM_HEADER_SIZE); + // TODO: Implement parsing for enhanced AC-3 with multiple sub-streams. + return 0; + } + + private CommonMp4AtomParsers() { + // Prevent instantiation. + } + + /** Represents the format for AC-3 audio. */ + private static final class Ac3Format { + + public final int channelCount; + public final int sampleRate; + public final int bitrate; + + public Ac3Format(int channelCount, int sampleRate, int bitrate) { + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.bitrate = bitrate; + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java new file mode 100644 index 0000000000..471689e24a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.mp4; + +import com.google.android.exoplayer.util.CodecSpecificDataUtil; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.nio.ByteBuffer; + +/** + * Utility methods and constants for parsing fragmented and unfragmented MP4 files. + */ +public final class Mp4Util { + + /** Size of an atom header, in bytes. */ + public static final int ATOM_HEADER_SIZE = 8; + + /** Size of a long atom header, in bytes. */ + public static final int LONG_ATOM_HEADER_SIZE = 16; + + /** Size of a full atom header, in bytes. */ + public static final int FULL_ATOM_HEADER_SIZE = 12; + + /** Four initial bytes that must prefix H.264/AVC NAL units for decoding. */ + private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; + + /** Parses the version number out of the additional integer component of a full atom. */ + public static int parseFullAtomVersion(int fullAtomInt) { + return 0x000000FF & (fullAtomInt >> 24); + } + + /** Parses the atom flags out of the additional integer component of a full atom. */ + public static int parseFullAtomFlags(int fullAtomInt) { + return 0x00FFFFFF & fullAtomInt; + } + + /** + * Reads an unsigned integer into an integer. This method is suitable for use when it can be + * assumed that the top bit will always be set to zero. + * + * @throws IllegalArgumentException If the top bit of the input data is set. + */ + public static int readUnsignedIntToInt(ByteBuffer data) { + int result = 0xFF & data.get(); + for (int i = 1; i < 4; i++) { + result <<= 8; + result |= 0xFF & data.get(); + } + if (result < 0) { + throw new IllegalArgumentException("Top bit not zero: " + result); + } + return result; + } + + /** + * Replaces length prefixes of NAL units in {@code buffer} with start code prefixes, within the + * {@code size} bytes preceding the buffer's position. + */ + public static void replaceLengthPrefixesWithAvcStartCodes(ByteBuffer buffer, int size) { + int sampleOffset = buffer.position() - size; + int position = sampleOffset; + while (position < sampleOffset + size) { + buffer.position(position); + int length = readUnsignedIntToInt(buffer); + buffer.position(position); + buffer.put(NAL_START_CODE); + position += length + 4; + } + buffer.position(sampleOffset + size); + } + + /** Constructs and returns a NAL unit with a start code followed by the data in {@code atom}. */ + public static byte[] parseChildNalUnit(ParsableByteArray atom) { + int length = atom.readUnsignedShort(); + int offset = atom.getPosition(); + atom.skip(length); + return CodecSpecificDataUtil.buildNalUnit(atom.data, offset, length); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Track.java b/library/src/main/java/com/google/android/exoplayer/mp4/Track.java index f718ec17b9..313e3272f6 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Track.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Track.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.mp4; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox; @@ -43,6 +44,10 @@ public final class Track { * Type of a meta track. */ public static final int TYPE_META = 0x6D657461; + /** + * Type of a time-code track. + */ + public static final int TYPE_TIME_CODE = 0x746D6364; /** * The track identifier. @@ -50,7 +55,8 @@ public final class Track { public final int id; /** - * One of {@link #TYPE_VIDEO}, {@link #TYPE_AUDIO}, {@link #TYPE_HINT} and {@link #TYPE_META}. + * One of {@link #TYPE_VIDEO}, {@link #TYPE_AUDIO}, {@link #TYPE_HINT}, {@link #TYPE_META} and + * {@link #TYPE_TIME_CODE}. */ public final int type; @@ -59,6 +65,11 @@ public final class Track { */ public final long timescale; + /** + * The duration of the track in microseconds, or {@link C#UNKNOWN_TIME_US} if unknown. + */ + public final long durationUs; + /** * The format if {@link #type} is {@link #TYPE_VIDEO} or {@link #TYPE_AUDIO}. Null otherwise. */ @@ -69,11 +80,12 @@ public final class Track { */ public final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes; - public Track(int id, int type, long timescale, MediaFormat mediaFormat, + public Track(int id, int type, long timescale, long durationUs, MediaFormat mediaFormat, TrackEncryptionBox[] sampleDescriptionEncryptionBoxes) { this.id = id; this.type = type; this.timescale = timescale; + this.durationUs = durationUs; this.mediaFormat = mediaFormat; this.sampleDescriptionEncryptionBoxes = sampleDescriptionEncryptionBoxes; } 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 7cc4384649..6e04658ef9 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 @@ -167,8 +167,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { : Track.TYPE_AUDIO; FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME); - extractor.setTrack(new Track(trackIndex, trackType, streamElement.timescale, mediaFormat, - trackEncryptionBoxes)); + extractor.setTrack(new Track(trackIndex, trackType, streamElement.timescale, + initialManifest.durationUs, mediaFormat, trackEncryptionBoxes)); extractors.put(trackIndex, extractor); } this.maxHeight = maxHeight;