Search in sources :

Example 1 with GaplessInfoHolder

use of com.google.android.exoplayer2.extractor.GaplessInfoHolder in project ExoPlayer by google.

the class AtomParsers method parseStbl.

/**
   * Parses an stbl atom (defined in 14496-12).
   *
   * @param track Track to which this sample table corresponds.
   * @param stblAtom stbl (sample table) atom to decode.
   * @param gaplessInfoHolder Holder to populate with gapless playback information.
   * @return Sample table described by the stbl atom.
   * @throws ParserException If the resulting sample sequence does not contain a sync sample.
   */
public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder) throws ParserException {
    SampleSizeBox sampleSizeBox;
    Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz);
    if (stszAtom != null) {
        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;
    Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco);
    if (chunkOffsetsAtom == null) {
        chunkOffsetsAreLongs = true;
        chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_co64);
    }
    ParsableByteArray chunkOffsets = chunkOffsetsAtom.data;
    // Entries are (chunk number, number of samples per chunk, sample description index).
    ParsableByteArray stsc = stblAtom.getLeafAtomOfType(Atom.TYPE_stsc).data;
    // Entries are (number of samples, timestamp delta between those samples).
    ParsableByteArray stts = stblAtom.getLeafAtomOfType(Atom.TYPE_stts).data;
    // Entries are the indices of samples that are synchronization samples.
    Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss);
    ParsableByteArray stss = stssAtom != null ? stssAtom.data : null;
    // Entries are (number of samples, timestamp offset).
    Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts);
    ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null;
    // Prepare to read chunk information.
    ChunkIterator chunkIterator = new ChunkIterator(stsc, chunkOffsets, chunkOffsetsAreLongs);
    // Prepare to read sample timestamps.
    stts.setPosition(Atom.FULL_HEADER_SIZE);
    int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1;
    int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
    int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
    // Prepare to read sample timestamp offsets, if ctts is present.
    int remainingSamplesAtTimestampOffset = 0;
    int remainingTimestampOffsetChanges = 0;
    int timestampOffset = 0;
    if (ctts != null) {
        ctts.setPosition(Atom.FULL_HEADER_SIZE);
        remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt();
    }
    int nextSynchronizationSampleIndex = C.INDEX_UNSET;
    int remainingSynchronizationSamples = 0;
    if (stss != null) {
        stss.setPosition(Atom.FULL_HEADER_SIZE);
        remainingSynchronizationSamples = stss.readUnsignedIntToInt();
        if (remainingSynchronizationSamples > 0) {
            nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
        } else {
            // Ignore empty stss boxes, which causes all samples to be treated as sync samples.
            stss = null;
        }
    }
    // True if we can rechunk fixed-sample-size data. Note that we only rechunk raw audio.
    boolean isRechunkable = sampleSizeBox.isFixedSampleSize() && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType) && remainingTimestampDeltaChanges == 0 && remainingTimestampOffsetChanges == 0 && remainingSynchronizationSamples == 0;
    long[] offsets;
    int[] sizes;
    int maximumSize = 0;
    long[] timestamps;
    int[] flags;
    long timestampTimeUnits = 0;
    if (!isRechunkable) {
        offsets = new long[sampleCount];
        sizes = new int[sampleCount];
        timestamps = new long[sampleCount];
        flags = new int[sampleCount];
        long offset = 0;
        int remainingSamplesInChunk = 0;
        for (int i = 0; i < sampleCount; i++) {
            // Advance to the next chunk if necessary.
            while (remainingSamplesInChunk == 0) {
                Assertions.checkState(chunkIterator.moveNext());
                offset = chunkIterator.offset;
                remainingSamplesInChunk = chunkIterator.numSamples;
            }
            // Add on the timestamp offset if ctts is present.
            if (ctts != null) {
                while (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) {
                    remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
                    // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers
                    // in version 0 ctts boxes, however some streams violate the spec and use signed
                    // integers instead. It's safe to always decode sample offsets as signed integers here,
                    // because unsigned integers will still be parsed correctly (unless their top bit is
                    // set, which is never true in practice because sample offsets are always small).
                    timestampOffset = ctts.readInt();
                    remainingTimestampOffsetChanges--;
                }
                remainingSamplesAtTimestampOffset--;
            }
            offsets[i] = offset;
            sizes[i] = sampleSizeBox.readNextSampleSize();
            if (sizes[i] > maximumSize) {
                maximumSize = sizes[i];
            }
            timestamps[i] = timestampTimeUnits + timestampOffset;
            // All samples are synchronization samples if the stss is not present.
            flags[i] = stss == null ? C.BUFFER_FLAG_KEY_FRAME : 0;
            if (i == nextSynchronizationSampleIndex) {
                flags[i] = C.BUFFER_FLAG_KEY_FRAME;
                remainingSynchronizationSamples--;
                if (remainingSynchronizationSamples > 0) {
                    nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
                }
            }
            // Add on the duration of this sample.
            timestampTimeUnits += timestampDeltaInTimeUnits;
            remainingSamplesAtTimestampDelta--;
            if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) {
                remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
                timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
                remainingTimestampDeltaChanges--;
            }
            offset += sizes[i];
            remainingSamplesInChunk--;
        }
        Assertions.checkArgument(remainingSamplesAtTimestampOffset == 0);
        // Remove trailing ctts entries with 0-valued sample counts.
        while (remainingTimestampOffsetChanges > 0) {
            Assertions.checkArgument(ctts.readUnsignedIntToInt() == 0);
            // Ignore offset.
            ctts.readInt();
            remainingTimestampOffsetChanges--;
        }
        // still be playable.
        if (remainingSynchronizationSamples != 0 || remainingSamplesAtTimestampDelta != 0 || remainingSamplesInChunk != 0 || remainingTimestampDeltaChanges != 0) {
            Log.w(TAG, "Inconsistent stbl box for track " + track.id + ": remainingSynchronizationSamples " + remainingSynchronizationSamples + ", remainingSamplesAtTimestampDelta " + remainingSamplesAtTimestampDelta + ", remainingSamplesInChunk " + remainingSamplesInChunk + ", remainingTimestampDeltaChanges " + remainingTimestampDeltaChanges);
        }
    } else {
        long[] chunkOffsetsBytes = new long[chunkIterator.length];
        int[] chunkSampleCounts = new int[chunkIterator.length];
        while (chunkIterator.moveNext()) {
            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;
        sizes = rechunkedResults.sizes;
        maximumSize = rechunkedResults.maximumSize;
        timestamps = rechunkedResults.timestamps;
        flags = rechunkedResults.flags;
    }
    if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) {
        // There is no edit list, or we are ignoring it as we already have gapless metadata to apply.
        // This implementation does not support applying both gapless metadata and an edit list.
        Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
        return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
    }
    if (track.editListDurations.length == 1 && track.type == C.TRACK_TYPE_AUDIO && timestamps.length >= 2) {
        // Handle the edit by setting gapless playback metadata, if possible. This implementation
        // assumes that only one "roll" sample is needed, which is the case for AAC, so the start/end
        // points of the edit must lie within the first/last samples respectively.
        long editStartTime = track.editListMediaTimes[0];
        long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0], track.timescale, track.movieTimescale);
        long lastSampleEndTime = timestampTimeUnits;
        if (timestamps[0] <= editStartTime && editStartTime < timestamps[1] && timestamps[timestamps.length - 1] < editEndTime && editEndTime <= lastSampleEndTime) {
            long paddingTimeUnits = lastSampleEndTime - editEndTime;
            long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0], track.format.sampleRate, track.timescale);
            long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits, track.format.sampleRate, track.timescale);
            if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE && encoderPadding <= Integer.MAX_VALUE) {
                gaplessInfoHolder.encoderDelay = (int) encoderDelay;
                gaplessInfoHolder.encoderPadding = (int) encoderPadding;
                Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
                return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
            }
        }
    }
    if (track.editListDurations.length == 1 && track.editListDurations[0] == 0) {
        // samples in the edit.
        for (int i = 0; i < timestamps.length; i++) {
            timestamps[i] = Util.scaleLargeTimestamp(timestamps[i] - track.editListMediaTimes[0], C.MICROS_PER_SECOND, track.timescale);
        }
        return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
    }
    // Omit any sample at the end point of an edit for audio tracks.
    boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO;
    // Count the number of samples after applying edits.
    int editedSampleCount = 0;
    int nextSampleIndex = 0;
    boolean copyMetadata = false;
    for (int i = 0; i < track.editListDurations.length; i++) {
        long mediaTime = track.editListMediaTimes[i];
        if (mediaTime != -1) {
            long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale, track.movieTimescale);
            int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
            int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample, false);
            editedSampleCount += endIndex - startIndex;
            copyMetadata |= nextSampleIndex != startIndex;
            nextSampleIndex = endIndex;
        }
    }
    copyMetadata |= editedSampleCount != sampleCount;
    // Calculate edited sample timestamps and update the corresponding metadata arrays.
    long[] editedOffsets = copyMetadata ? new long[editedSampleCount] : offsets;
    int[] editedSizes = copyMetadata ? new int[editedSampleCount] : sizes;
    int editedMaximumSize = copyMetadata ? 0 : maximumSize;
    int[] editedFlags = copyMetadata ? new int[editedSampleCount] : flags;
    long[] editedTimestamps = new long[editedSampleCount];
    long pts = 0;
    int sampleIndex = 0;
    for (int i = 0; i < track.editListDurations.length; i++) {
        long mediaTime = track.editListMediaTimes[i];
        long duration = track.editListDurations[i];
        if (mediaTime != -1) {
            long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale, track.movieTimescale);
            int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
            int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false);
            if (copyMetadata) {
                int count = endIndex - startIndex;
                System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count);
                System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count);
                System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count);
            }
            for (int j = startIndex; j < endIndex; j++) {
                long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
                long timeInSegmentUs = Util.scaleLargeTimestamp(timestamps[j] - mediaTime, C.MICROS_PER_SECOND, track.timescale);
                editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs;
                if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) {
                    editedMaximumSize = sizes[j];
                }
                sampleIndex++;
            }
        }
        pts += duration;
    }
    boolean hasSyncSample = false;
    for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) {
        hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0;
    }
    if (!hasSyncSample) {
        throw new ParserException("The edited sample sequence does not contain a sync sample.");
    }
    return new TrackSampleTable(editedOffsets, editedSizes, editedMaximumSize, editedTimestamps, editedFlags);
}
Also used : ParserException(com.google.android.exoplayer2.ParserException) ParsableByteArray(com.google.android.exoplayer2.util.ParsableByteArray)

Example 2 with GaplessInfoHolder

use of com.google.android.exoplayer2.extractor.GaplessInfoHolder in project ExoPlayer by google.

the class Mp4Extractor method processMoovAtom.

/**
   * Updates the stored track metadata to reflect the contents of the specified moov atom.
   */
private void processMoovAtom(ContainerAtom moov) throws ParserException {
    long durationUs = C.TIME_UNSET;
    List<Mp4Track> tracks = new ArrayList<>();
    long earliestSampleOffset = Long.MAX_VALUE;
    Metadata metadata = null;
    GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
    Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
    if (udta != null) {
        metadata = AtomParsers.parseUdta(udta, isQuickTime);
        if (metadata != null) {
            gaplessInfoHolder.setFromMetadata(metadata);
        }
    }
    for (int i = 0; i < moov.containerChildren.size(); i++) {
        Atom.ContainerAtom atom = moov.containerChildren.get(i);
        if (atom.type != Atom.TYPE_trak) {
            continue;
        }
        Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), C.TIME_UNSET, null, isQuickTime);
        if (track == null) {
            continue;
        }
        Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia).getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
        TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
        if (trackSampleTable.sampleCount == 0) {
            continue;
        }
        Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i, track.type));
        // Each sample has up to three bytes of overhead for the start code that replaces its length.
        // Allow ten source samples per output sample, like the platform extractor.
        int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
        Format format = track.format.copyWithMaxInputSize(maxInputSize);
        if (track.type == C.TRACK_TYPE_AUDIO) {
            if (gaplessInfoHolder.hasGaplessInfo()) {
                format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding);
            }
            if (metadata != null) {
                format = format.copyWithMetadata(metadata);
            }
        }
        mp4Track.trackOutput.format(format);
        durationUs = Math.max(durationUs, track.durationUs);
        tracks.add(mp4Track);
        long firstSampleOffset = trackSampleTable.offsets[0];
        if (firstSampleOffset < earliestSampleOffset) {
            earliestSampleOffset = firstSampleOffset;
        }
    }
    this.durationUs = durationUs;
    this.tracks = tracks.toArray(new Mp4Track[tracks.size()]);
    extractorOutput.endTracks();
    extractorOutput.seekMap(this);
}
Also used : ArrayList(java.util.ArrayList) Metadata(com.google.android.exoplayer2.metadata.Metadata) ContainerAtom(com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom) ContainerAtom(com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom) Format(com.google.android.exoplayer2.Format) GaplessInfoHolder(com.google.android.exoplayer2.extractor.GaplessInfoHolder)

Aggregations

Format (com.google.android.exoplayer2.Format)1 ParserException (com.google.android.exoplayer2.ParserException)1 GaplessInfoHolder (com.google.android.exoplayer2.extractor.GaplessInfoHolder)1 ContainerAtom (com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom)1 Metadata (com.google.android.exoplayer2.metadata.Metadata)1 ParsableByteArray (com.google.android.exoplayer2.util.ParsableByteArray)1 ArrayList (java.util.ArrayList)1