diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java new file mode 100644 index 0000000000..2d32839c51 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 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.exoplayer2.extractor.mp4; + +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; + +import junit.framework.TestCase; + +/** + * Tests for {@link AtomParsers}. + */ +public final class AtomParsersTest extends TestCase { + + private static final String ATOM_HEADER = "000000000000000000000000"; + private static final String SAMPLE_COUNT = "00000004"; + private static final byte[] FOUR_BIT_STZ2 = Util.getBytesFromHexString(ATOM_HEADER + "00000004" + + SAMPLE_COUNT + "1234"); + private static final byte[] EIGHT_BIT_STZ2 = Util.getBytesFromHexString(ATOM_HEADER + "00000008" + + SAMPLE_COUNT + "01020304"); + private static final byte[] SIXTEEN_BIT_STZ2 = Util.getBytesFromHexString(ATOM_HEADER + "00000010" + + SAMPLE_COUNT + "0001000200030004"); + + public void testStz2Parsing4BitFieldSize() { + verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(FOUR_BIT_STZ2))); + } + + public void testStz2Parsing8BitFieldSize() { + verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(EIGHT_BIT_STZ2))); + } + + public void testStz2Parsing16BitFieldSize() { + verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(SIXTEEN_BIT_STZ2))); + } + + private void verifyParsing(Atom.LeafAtom stz2Atom) { + AtomParsers.Stz2SampleSizeBox box = new AtomParsers.Stz2SampleSizeBox(stz2Atom); + assertEquals(4, box.getSampleCount()); + assertFalse(box.isFixedSampleSize()); + for (int i = 0; i < box.getSampleCount(); i++) { + assertEquals(i + 1, box.readNextSampleSize()); + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 7f494aad8f..915f21f3ce 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -111,6 +111,7 @@ import java.util.List; public static final int TYPE_ctts = Util.getIntegerCodeForString("ctts"); public static final int TYPE_stsc = Util.getIntegerCodeForString("stsc"); public static final int TYPE_stsz = Util.getIntegerCodeForString("stsz"); + public static final int TYPE_stz2 = Util.getIntegerCodeForString("stz2"); public static final int TYPE_stco = Util.getIntegerCodeForString("stco"); public static final int TYPE_co64 = Util.getIntegerCodeForString("co64"); public static final int TYPE_tx3g = Util.getIntegerCodeForString("tx3g"); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index a5d7fb24f4..39149fde69 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -50,7 +50,7 @@ import java.util.List; * @param trak Atom to decode. * @param mvhd Movie header atom, used to get the timescale. * @param duration The duration in units of the timescale declared in the mvhd atom, or -1 if the - * duration should be parsed from the tkhd atom. + * duration should be parsed from the tkhd atom. * @param drmInitData {@link DrmInitData} to be included in the format. * @param isQuickTime True for QuickTime media. False otherwise. * @return A {@link Track} instance, or {@code null} if the track's type isn't supported. @@ -98,8 +98,22 @@ import java.util.List; */ public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder) throws ParserException { - // Array of sample sizes. - ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data; + SampleSizeBox sampleSizeBox; + Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); + if (stszAtom != null) { + sampleSizeBox = new StszSampleSizeBox(stszAtom); + } else { + Atom.LeafAtom stz2Atom = stblAtom.getLeafAtomOfType(Atom.TYPE_stz2); + if (stz2Atom == null) { + throw new ParserException("Track has no sample table size information"); + } + sampleSizeBox = new Stz2SampleSizeBox(stz2Atom); + } + + int sampleCount = sampleSizeBox.getSampleCount(); + if (sampleCount == 0) { + return new TrackSampleTable(new long[0], new int[0], 0, new long[0], new int[0]); + } // Entries are byte offsets of chunks. boolean chunkOffsetsAreLongs = false; @@ -120,14 +134,6 @@ import java.util.List; Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts); ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null; - // Skip full atom. - stsz.setPosition(Atom.FULL_HEADER_SIZE); - int fixedSampleSize = stsz.readUnsignedIntToInt(); - int sampleCount = stsz.readUnsignedIntToInt(); - if (sampleCount == 0) { - return new TrackSampleTable(new long[0], new int[0], 0, new long[0], new int[0]); - } - // Prepare to read chunk information. ChunkIterator chunkIterator = new ChunkIterator(stsc, chunkOffsets, chunkOffsetsAreLongs); @@ -160,7 +166,7 @@ import java.util.List; } // True if we can rechunk fixed-sample-size data. Note that we only rechunk raw audio. - boolean isRechunkable = fixedSampleSize != 0 + boolean isRechunkable = sampleSizeBox.isFixedSampleSize() && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType) && remainingTimestampDeltaChanges == 0 && remainingTimestampOffsetChanges == 0 && remainingSynchronizationSamples == 0; @@ -204,7 +210,7 @@ import java.util.List; } offsets[i] = offset; - sizes[i] = fixedSampleSize == 0 ? stsz.readUnsignedIntToInt() : fixedSampleSize; + sizes[i] = sampleSizeBox.readNextSampleSize(); if (sizes[i] > maximumSize) { maximumSize = sizes[i]; } @@ -253,6 +259,7 @@ import java.util.List; chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset; chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples; } + int fixedSampleSize = sampleSizeBox.readNextSampleSize(); FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk( fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits); offsets = rechunkedResults.offsets; @@ -561,7 +568,7 @@ import java.util.List; * * @param mdhd The mdhd atom to decode. * @return A pair consisting of the media timescale defined as the number of time units that pass - * in one second, and the language code. + * in one second, and the language code. */ private static Pair parseMdhd(ParsableByteArray mdhd) { mdhd.setPosition(Atom.HEADER_SIZE); @@ -717,7 +724,7 @@ import java.util.List; * * @param edtsAtom edts (edit box) atom to decode. * @return Pair of edit list durations and edit list media times, or a pair of nulls if they are - * not present. + * not present. */ private static Pair parseEdts(Atom.ContainerAtom edtsAtom) { Atom.LeafAtom elst; @@ -867,7 +874,9 @@ import java.util.List; } } - /** Returns the position of the esds box within a parent, or -1 if no esds box is found */ + /** + * Returns the position of the esds box within a parent, or -1 if no esds box is found + */ private static int findEsdsPosition(ParsableByteArray parent, int position, int size) { int childAtomPosition = parent.getPosition(); while (childAtomPosition - position < size) { @@ -883,7 +892,9 @@ import java.util.List; return -1; } - /** Returns codec-specific initialization data contained in an esds box. */ + /** + * Returns codec-specific initialization data contained in an esds box. + */ private static Pair parseEsdsFromParent(ParsableByteArray parent, int position) { parent.setPosition(position + Atom.HEADER_SIZE + 4); // Start of the ES_Descriptor (defined in 14496-1) @@ -1028,7 +1039,9 @@ import java.util.List; return null; } - /** Parses the size of an expandable class, as specified by ISO 14496-1 subsection 8.3.3. */ + /** + * Parses the size of an expandable class, as specified by ISO 14496-1 subsection 8.3.3. + */ private static int parseExpandableClassSize(ParsableByteArray data) { int currentByte = data.readUnsignedByte(); int size = currentByte & 0x7F; @@ -1122,6 +1135,114 @@ import java.util.List; requiredSampleTransformation = Track.TRANSFORMATION_NONE; } + + } + + /** + * A box containing sample sizes (e.g. stsz, stz2). + */ + private interface SampleSizeBox { + + /** + * Returns the number of samples. + */ + int getSampleCount(); + + /** + * Returns the size for the next sample. + */ + int readNextSampleSize(); + + /** + * Returns whether samples have a fixed size. + */ + boolean isFixedSampleSize(); + + } + + /** + * An stsz sample size box. + */ + /* package */ static final class StszSampleSizeBox implements SampleSizeBox { + + private final int fixedSampleSize; + private final int sampleCount; + private final ParsableByteArray data; + + public StszSampleSizeBox(Atom.LeafAtom stszAtom) { + data = stszAtom.data; + data.setPosition(Atom.FULL_HEADER_SIZE); + fixedSampleSize = data.readUnsignedIntToInt(); + sampleCount = data.readUnsignedIntToInt(); + } + + @Override + public int getSampleCount() { + return sampleCount; + } + + @Override + public int readNextSampleSize() { + return fixedSampleSize == 0 ? data.readUnsignedIntToInt() : fixedSampleSize; + } + + @Override + public boolean isFixedSampleSize() { + return fixedSampleSize != 0; + } + + } + + /** + * An stz2 sample size box. + */ + /* package */ static final class Stz2SampleSizeBox implements SampleSizeBox { + + private final ParsableByteArray data; + private final int sampleCount; + private final int fieldSize; // Can be 4, 8, or 16. + + // Used only if fieldSize == 4. + private int sampleIndex; + private int currentByte; + + public Stz2SampleSizeBox(Atom.LeafAtom stz2Atom) { + data = stz2Atom.data; + data.setPosition(Atom.FULL_HEADER_SIZE); + fieldSize = data.readUnsignedIntToInt() & 0x000000FF; + sampleCount = data.readUnsignedIntToInt(); + } + + @Override + public int getSampleCount() { + return sampleCount; + } + + @Override + public int readNextSampleSize() { + if (fieldSize == 8) { + return data.readUnsignedByte(); + } else if (fieldSize == 16) { + return data.readUnsignedShort(); + } else { + // fieldSize == 4. + if ((sampleIndex++ % 2) == 0) { + // Read the next byte into our cached byte when we are reading the upper bits. + currentByte = data.readUnsignedByte(); + // Read the upper bits from the byte and shift them to the lower 4 bits. + return (currentByte & 0xF0) >> 4; + } else { + // Mask out the upper 4 bits of the last byte we read. + return currentByte & 0x0F; + } + } + } + + @Override + public boolean isFixedSampleSize() { + return false; + } + } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 0c2b1beed3..a0674dbb2d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -475,8 +475,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr || atom == Atom.TYPE_stsd || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss || atom == Atom.TYPE_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc - || atom == Atom.TYPE_stsz || atom == Atom.TYPE_stco || atom == Atom.TYPE_co64 - || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp || atom == Atom.TYPE_udta; + || atom == Atom.TYPE_stsz || atom == Atom.TYPE_stz2 || atom == Atom.TYPE_stco + || atom == Atom.TYPE_co64 || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp + || atom == Atom.TYPE_udta; } /**