| /* |
| * 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 android.util.Pair; |
| import android.util.SparseArray; |
| import androidx.annotation.IntDef; |
| import androidx.annotation.Nullable; |
| import com.google.android.exoplayer2.C; |
| import com.google.android.exoplayer2.Format; |
| import com.google.android.exoplayer2.ParserException; |
| import com.google.android.exoplayer2.audio.Ac4Util; |
| import com.google.android.exoplayer2.drm.DrmInitData; |
| import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; |
| import com.google.android.exoplayer2.extractor.CeaUtil; |
| import com.google.android.exoplayer2.extractor.ChunkIndex; |
| import com.google.android.exoplayer2.extractor.Extractor; |
| import com.google.android.exoplayer2.extractor.ExtractorInput; |
| import com.google.android.exoplayer2.extractor.ExtractorOutput; |
| import com.google.android.exoplayer2.extractor.ExtractorsFactory; |
| import com.google.android.exoplayer2.extractor.PositionHolder; |
| import com.google.android.exoplayer2.extractor.SeekMap; |
| import com.google.android.exoplayer2.extractor.TrackOutput; |
| import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; |
| import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom; |
| import com.google.android.exoplayer2.metadata.emsg.EventMessage; |
| import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; |
| import com.google.android.exoplayer2.util.Assertions; |
| import com.google.android.exoplayer2.util.Log; |
| import com.google.android.exoplayer2.util.MimeTypes; |
| import com.google.android.exoplayer2.util.NalUnitUtil; |
| import com.google.android.exoplayer2.util.ParsableByteArray; |
| import com.google.android.exoplayer2.util.TimestampAdjuster; |
| import com.google.android.exoplayer2.util.Util; |
| import java.io.IOException; |
| import java.lang.annotation.Documented; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.UUID; |
| import org.checkerframework.checker.nullness.qual.MonotonicNonNull; |
| |
| /** Extracts data from the FMP4 container format. */ |
| @SuppressWarnings("ConstantField") |
| public class FragmentedMp4Extractor implements Extractor { |
| |
| /** Factory for {@link FragmentedMp4Extractor} instances. */ |
| public static final ExtractorsFactory FACTORY = |
| () -> new Extractor[] {new FragmentedMp4Extractor()}; |
| |
| /** |
| * Flags controlling the behavior of the extractor. Possible flag values are {@link |
| * #FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME}, {@link #FLAG_WORKAROUND_IGNORE_TFDT_BOX}, |
| * {@link #FLAG_ENABLE_EMSG_TRACK}, {@link #FLAG_SIDELOADED} and {@link |
| * #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}. |
| */ |
| @Documented |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef( |
| flag = true, |
| value = { |
| FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, |
| FLAG_WORKAROUND_IGNORE_TFDT_BOX, |
| FLAG_ENABLE_EMSG_TRACK, |
| FLAG_SIDELOADED, |
| FLAG_WORKAROUND_IGNORE_EDIT_LISTS |
| }) |
| public @interface Flags {} |
| /** |
| * Flag to work around an issue in some video streams where every frame is marked as a sync frame. |
| * The workaround overrides the sync frame flags in the stream, forcing them to false except for |
| * the first sample in each segment. |
| * <p> |
| * This flag does nothing if the stream is not a video stream. |
| */ |
| 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 FLAG_WORKAROUND_IGNORE_TFDT_BOX = 1 << 1; // 2 |
| /** |
| * Flag to indicate that the extractor should output an event message metadata track. Any event |
| * messages in the stream will be delivered as samples to this track. |
| */ |
| public static final int FLAG_ENABLE_EMSG_TRACK = 1 << 2; // 4 |
| /** |
| * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 |
| * container. |
| */ |
| private static final int FLAG_SIDELOADED = 1 << 3; // 8 |
| /** Flag to ignore any edit lists in the stream. */ |
| public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1 << 4; // 16 |
| |
| private static final String TAG = "FragmentedMp4Extractor"; |
| |
| @SuppressWarnings("ConstantCaseForConstants") |
| private static final int SAMPLE_GROUP_TYPE_seig = 0x73656967; |
| |
| 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}; |
| private static final Format EMSG_FORMAT = |
| new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_EMSG).build(); |
| |
| // 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; |
| |
| // Workarounds. |
| @Flags private final int flags; |
| @Nullable private final Track sideloadedTrack; |
| |
| // Sideloaded data. |
| private final List<Format> closedCaptionFormats; |
| |
| // Track-linked data bundle, accessible as a whole through trackID. |
| private final SparseArray<TrackBundle> trackBundles; |
| |
| // Temporary arrays. |
| private final ParsableByteArray nalStartCode; |
| private final ParsableByteArray nalPrefix; |
| private final ParsableByteArray nalBuffer; |
| private final byte[] scratchBytes; |
| private final ParsableByteArray scratch; |
| |
| // Adjusts sample timestamps. |
| @Nullable private final TimestampAdjuster timestampAdjuster; |
| |
| private final EventMessageEncoder eventMessageEncoder; |
| |
| // Parser state. |
| private final ParsableByteArray atomHeader; |
| private final ArrayDeque<ContainerAtom> containerAtoms; |
| private final ArrayDeque<MetadataSampleInfo> pendingMetadataSampleInfos; |
| @Nullable private final TrackOutput additionalEmsgTrackOutput; |
| |
| private int parserState; |
| private int atomType; |
| private long atomSize; |
| private int atomHeaderBytesRead; |
| @Nullable private ParsableByteArray atomData; |
| private long endOfMdatPosition; |
| private int pendingMetadataSampleBytes; |
| private long pendingSeekTimeUs; |
| |
| private long durationUs; |
| private long segmentIndexEarliestPresentationTimeUs; |
| @Nullable private TrackBundle currentTrackBundle; |
| private int sampleSize; |
| private int sampleBytesWritten; |
| private int sampleCurrentNalBytesRemaining; |
| private boolean processSeiNalUnitPayload; |
| |
| // Extractor output. |
| private @MonotonicNonNull ExtractorOutput extractorOutput; |
| private TrackOutput[] emsgTrackOutputs; |
| private TrackOutput[] cea608TrackOutputs; |
| |
| // Whether extractorOutput.seekMap has been called. |
| private boolean haveOutputSeekMap; |
| |
| public FragmentedMp4Extractor() { |
| this(0); |
| } |
| |
| /** |
| * @param flags Flags that control the extractor's behavior. |
| */ |
| public FragmentedMp4Extractor(@Flags int flags) { |
| this(flags, /* timestampAdjuster= */ null); |
| } |
| |
| /** |
| * @param flags Flags that control the extractor's behavior. |
| * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. |
| */ |
| public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) { |
| this(flags, timestampAdjuster, /* sideloadedTrack= */ null, Collections.emptyList()); |
| } |
| |
| /** |
| * @param flags Flags that control the extractor's behavior. |
| * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. |
| * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not |
| * receive a moov box in the input data. Null if a moov box is expected. |
| */ |
| public FragmentedMp4Extractor( |
| @Flags int flags, |
| @Nullable TimestampAdjuster timestampAdjuster, |
| @Nullable Track sideloadedTrack) { |
| this(flags, timestampAdjuster, sideloadedTrack, Collections.emptyList()); |
| } |
| |
| /** |
| * @param flags Flags that control the extractor's behavior. |
| * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. |
| * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not |
| * receive a moov box in the input data. Null if a moov box is expected. |
| * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed |
| * caption channels to expose. |
| */ |
| public FragmentedMp4Extractor( |
| @Flags int flags, |
| @Nullable TimestampAdjuster timestampAdjuster, |
| @Nullable Track sideloadedTrack, |
| List<Format> closedCaptionFormats) { |
| this( |
| flags, |
| timestampAdjuster, |
| sideloadedTrack, |
| closedCaptionFormats, |
| /* additionalEmsgTrackOutput= */ null); |
| } |
| |
| /** |
| * @param flags Flags that control the extractor's behavior. |
| * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. |
| * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not |
| * receive a moov box in the input data. Null if a moov box is expected. |
| * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed |
| * caption channels to expose. |
| * @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages |
| * targeting the player, even if {@link #FLAG_ENABLE_EMSG_TRACK} is not set. Null if special |
| * handling of emsg messages for players is not required. |
| */ |
| public FragmentedMp4Extractor( |
| @Flags int flags, |
| @Nullable TimestampAdjuster timestampAdjuster, |
| @Nullable Track sideloadedTrack, |
| List<Format> closedCaptionFormats, |
| @Nullable TrackOutput additionalEmsgTrackOutput) { |
| this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); |
| this.timestampAdjuster = timestampAdjuster; |
| this.sideloadedTrack = sideloadedTrack; |
| this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); |
| this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; |
| eventMessageEncoder = new EventMessageEncoder(); |
| atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); |
| nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); |
| nalPrefix = new ParsableByteArray(5); |
| nalBuffer = new ParsableByteArray(); |
| scratchBytes = new byte[16]; |
| scratch = new ParsableByteArray(scratchBytes); |
| containerAtoms = new ArrayDeque<>(); |
| pendingMetadataSampleInfos = new ArrayDeque<>(); |
| trackBundles = new SparseArray<>(); |
| durationUs = C.TIME_UNSET; |
| pendingSeekTimeUs = C.TIME_UNSET; |
| segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET; |
| enterReadingAtomHeaderState(); |
| } |
| |
| @Override |
| public boolean sniff(ExtractorInput input) throws IOException { |
| return Sniffer.sniffFragmented(input); |
| } |
| |
| @Override |
| public void init(ExtractorOutput output) { |
| extractorOutput = output; |
| if (sideloadedTrack != null) { |
| TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type)); |
| bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); |
| trackBundles.put(0, bundle); |
| maybeInitExtraTracks(); |
| extractorOutput.endTracks(); |
| } |
| } |
| |
| @Override |
| public void seek(long position, long timeUs) { |
| int trackCount = trackBundles.size(); |
| for (int i = 0; i < trackCount; i++) { |
| trackBundles.valueAt(i).reset(); |
| } |
| pendingMetadataSampleInfos.clear(); |
| pendingMetadataSampleBytes = 0; |
| pendingSeekTimeUs = timeUs; |
| containerAtoms.clear(); |
| enterReadingAtomHeaderState(); |
| } |
| |
| @Override |
| public void release() { |
| // Do nothing |
| } |
| |
| @Override |
| public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { |
| while (true) { |
| switch (parserState) { |
| case STATE_READING_ATOM_HEADER: |
| if (!readAtomHeader(input)) { |
| return Extractor.RESULT_END_OF_INPUT; |
| } |
| break; |
| case STATE_READING_ATOM_PAYLOAD: |
| readAtomPayload(input); |
| break; |
| case STATE_READING_ENCRYPTION_DATA: |
| readEncryptionData(input); |
| break; |
| default: |
| if (readSample(input)) { |
| return RESULT_CONTINUE; |
| } |
| } |
| } |
| } |
| |
| private void enterReadingAtomHeaderState() { |
| parserState = STATE_READING_ATOM_HEADER; |
| atomHeaderBytesRead = 0; |
| } |
| |
| private boolean readAtomHeader(ExtractorInput input) throws IOException { |
| if (atomHeaderBytesRead == 0) { |
| // Read the standard length atom header. |
| if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) { |
| return false; |
| } |
| atomHeaderBytesRead = Atom.HEADER_SIZE; |
| atomHeader.setPosition(0); |
| atomSize = atomHeader.readUnsignedInt(); |
| atomType = atomHeader.readInt(); |
| } |
| |
| if (atomSize == Atom.DEFINES_LARGE_SIZE) { |
| // Read the large size. |
| int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; |
| input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); |
| atomHeaderBytesRead += headerBytesRemaining; |
| atomSize = atomHeader.readUnsignedLongToLong(); |
| } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { |
| // The atom extends to the end of the file. Note that if the atom is within a container we can |
| // work out its size even if the input length is unknown. |
| long endPosition = input.getLength(); |
| if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) { |
| endPosition = containerAtoms.peek().endPosition; |
| } |
| if (endPosition != C.LENGTH_UNSET) { |
| atomSize = endPosition - input.getPosition() + atomHeaderBytesRead; |
| } |
| } |
| |
| if (atomSize < atomHeaderBytesRead) { |
| throw new ParserException("Atom size less than header length (unsupported)."); |
| } |
| |
| long atomPosition = input.getPosition() - atomHeaderBytesRead; |
| if (atomType == Atom.TYPE_moof) { |
| // The data positions may be updated when parsing the tfhd/trun. |
| int trackCount = trackBundles.size(); |
| for (int i = 0; i < trackCount; i++) { |
| TrackFragment fragment = trackBundles.valueAt(i).fragment; |
| fragment.atomPosition = atomPosition; |
| fragment.auxiliaryDataPosition = atomPosition; |
| fragment.dataPosition = atomPosition; |
| } |
| } |
| |
| if (atomType == Atom.TYPE_mdat) { |
| currentTrackBundle = null; |
| endOfMdatPosition = atomPosition + atomSize; |
| if (!haveOutputSeekMap) { |
| // This must be the first mdat in the stream. |
| extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition)); |
| haveOutputSeekMap = true; |
| } |
| parserState = STATE_READING_ENCRYPTION_DATA; |
| return true; |
| } |
| |
| if (shouldParseContainerAtom(atomType)) { |
| long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE; |
| containerAtoms.push(new ContainerAtom(atomType, endPosition)); |
| if (atomSize == atomHeaderBytesRead) { |
| processAtomEnded(endPosition); |
| } else { |
| // Start reading the first child atom. |
| enterReadingAtomHeaderState(); |
| } |
| } else if (shouldParseLeafAtom(atomType)) { |
| if (atomHeaderBytesRead != Atom.HEADER_SIZE) { |
| throw new ParserException("Leaf atom defines extended atom size (unsupported)."); |
| } |
| if (atomSize > Integer.MAX_VALUE) { |
| throw new ParserException("Leaf atom with length > 2147483647 (unsupported)."); |
| } |
| atomData = new ParsableByteArray((int) atomSize); |
| System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE); |
| parserState = STATE_READING_ATOM_PAYLOAD; |
| } else { |
| if (atomSize > Integer.MAX_VALUE) { |
| throw new ParserException("Skipping atom with length > 2147483647 (unsupported)."); |
| } |
| atomData = null; |
| parserState = STATE_READING_ATOM_PAYLOAD; |
| } |
| |
| return true; |
| } |
| |
| private void readAtomPayload(ExtractorInput input) throws IOException { |
| int atomPayloadSize = (int) atomSize - atomHeaderBytesRead; |
| if (atomData != null) { |
| input.readFully(atomData.data, Atom.HEADER_SIZE, atomPayloadSize); |
| onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition()); |
| } else { |
| input.skipFully(atomPayloadSize); |
| } |
| processAtomEnded(input.getPosition()); |
| } |
| |
| private void processAtomEnded(long atomEndPosition) throws ParserException { |
| while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) { |
| onContainerAtomRead(containerAtoms.pop()); |
| } |
| enterReadingAtomHeaderState(); |
| } |
| |
| private void onLeafAtomRead(LeafAtom leaf, long inputPosition) throws ParserException { |
| if (!containerAtoms.isEmpty()) { |
| containerAtoms.peek().add(leaf); |
| } else if (leaf.type == Atom.TYPE_sidx) { |
| Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition); |
| segmentIndexEarliestPresentationTimeUs = result.first; |
| extractorOutput.seekMap(result.second); |
| haveOutputSeekMap = true; |
| } else if (leaf.type == Atom.TYPE_emsg) { |
| onEmsgLeafAtomRead(leaf.data); |
| } |
| } |
| |
| private void onContainerAtomRead(ContainerAtom container) throws ParserException { |
| if (container.type == Atom.TYPE_moov) { |
| onMoovContainerAtomRead(container); |
| } else if (container.type == Atom.TYPE_moof) { |
| onMoofContainerAtomRead(container); |
| } else if (!containerAtoms.isEmpty()) { |
| containerAtoms.peek().add(container); |
| } |
| } |
| |
| private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { |
| Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); |
| |
| @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren); |
| |
| // Read declaration of track fragments in the Moov box. |
| ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); |
| SparseArray<DefaultSampleValues> defaultSampleValuesArray = new SparseArray<>(); |
| long duration = C.TIME_UNSET; |
| 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<Integer, DefaultSampleValues> trexData = parseTrex(atom.data); |
| defaultSampleValuesArray.put(trexData.first, trexData.second); |
| } else if (atom.type == Atom.TYPE_mehd) { |
| duration = parseMehd(atom.data); |
| } |
| } |
| |
| // Construction of tracks. |
| SparseArray<Track> tracks = new SparseArray<>(); |
| int moovContainerChildrenSize = moov.containerChildren.size(); |
| for (int i = 0; i < moovContainerChildrenSize; i++) { |
| Atom.ContainerAtom atom = moov.containerChildren.get(i); |
| if (atom.type == Atom.TYPE_trak) { |
| @Nullable |
| Track track = |
| modifyTrack( |
| AtomParsers.parseTrak( |
| atom, |
| moov.getLeafAtomOfType(Atom.TYPE_mvhd), |
| duration, |
| drmInitData, |
| (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, |
| false)); |
| if (track != null) { |
| tracks.put(track.id, track); |
| } |
| } |
| } |
| |
| int trackCount = tracks.size(); |
| if (trackBundles.size() == 0) { |
| // We need to create the track bundles. |
| for (int i = 0; i < trackCount; i++) { |
| Track track = tracks.valueAt(i); |
| TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); |
| trackBundle.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id)); |
| trackBundles.put(track.id, trackBundle); |
| durationUs = Math.max(durationUs, track.durationUs); |
| } |
| maybeInitExtraTracks(); |
| extractorOutput.endTracks(); |
| } else { |
| Assertions.checkState(trackBundles.size() == trackCount); |
| for (int i = 0; i < trackCount; i++) { |
| Track track = tracks.valueAt(i); |
| trackBundles |
| .get(track.id) |
| .init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id)); |
| } |
| } |
| } |
| |
| @Nullable |
| protected Track modifyTrack(@Nullable Track track) { |
| return track; |
| } |
| |
| private DefaultSampleValues getDefaultSampleValues( |
| SparseArray<DefaultSampleValues> defaultSampleValuesArray, int trackId) { |
| if (defaultSampleValuesArray.size() == 1) { |
| // Ignore track id if there is only one track to cope with non-matching track indices. |
| // See https://github.com/google/ExoPlayer/issues/4477. |
| return defaultSampleValuesArray.valueAt(/* index= */ 0); |
| } |
| return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId)); |
| } |
| |
| private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { |
| parseMoof(moof, trackBundles, flags, scratchBytes); |
| |
| @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren); |
| if (drmInitData != null) { |
| int trackCount = trackBundles.size(); |
| for (int i = 0; i < trackCount; i++) { |
| trackBundles.valueAt(i).updateDrmInitData(drmInitData); |
| } |
| } |
| // If we have a pending seek, advance tracks to their preceding sync frames. |
| if (pendingSeekTimeUs != C.TIME_UNSET) { |
| int trackCount = trackBundles.size(); |
| for (int i = 0; i < trackCount; i++) { |
| trackBundles.valueAt(i).seek(pendingSeekTimeUs); |
| } |
| pendingSeekTimeUs = C.TIME_UNSET; |
| } |
| } |
| |
| private void maybeInitExtraTracks() { |
| if (emsgTrackOutputs == null) { |
| emsgTrackOutputs = new TrackOutput[2]; |
| int emsgTrackOutputCount = 0; |
| if (additionalEmsgTrackOutput != null) { |
| emsgTrackOutputs[emsgTrackOutputCount++] = additionalEmsgTrackOutput; |
| } |
| if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0) { |
| emsgTrackOutputs[emsgTrackOutputCount++] = |
| extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); |
| } |
| emsgTrackOutputs = Arrays.copyOf(emsgTrackOutputs, emsgTrackOutputCount); |
| |
| for (TrackOutput eventMessageTrackOutput : emsgTrackOutputs) { |
| eventMessageTrackOutput.format(EMSG_FORMAT); |
| } |
| } |
| if (cea608TrackOutputs == null) { |
| cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()]; |
| for (int i = 0; i < cea608TrackOutputs.length; i++) { |
| TrackOutput output = extractorOutput.track(trackBundles.size() + 1 + i, C.TRACK_TYPE_TEXT); |
| output.format(closedCaptionFormats.get(i)); |
| cea608TrackOutputs[i] = output; |
| } |
| } |
| } |
| |
| /** Handles an emsg atom (defined in 23009-1). */ |
| private void onEmsgLeafAtomRead(ParsableByteArray atom) { |
| if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) { |
| return; |
| } |
| atom.setPosition(Atom.HEADER_SIZE); |
| int fullAtom = atom.readInt(); |
| int version = Atom.parseFullAtomVersion(fullAtom); |
| String schemeIdUri; |
| String value; |
| long timescale; |
| long presentationTimeDeltaUs = C.TIME_UNSET; // Only set if version == 0 |
| long sampleTimeUs = C.TIME_UNSET; |
| long durationMs; |
| long id; |
| switch (version) { |
| case 0: |
| schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); |
| value = Assertions.checkNotNull(atom.readNullTerminatedString()); |
| timescale = atom.readUnsignedInt(); |
| presentationTimeDeltaUs = |
| Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); |
| if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { |
| sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs; |
| } |
| durationMs = |
| Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); |
| id = atom.readUnsignedInt(); |
| break; |
| case 1: |
| timescale = atom.readUnsignedInt(); |
| sampleTimeUs = |
| Util.scaleLargeTimestamp(atom.readUnsignedLongToLong(), C.MICROS_PER_SECOND, timescale); |
| durationMs = |
| Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); |
| id = atom.readUnsignedInt(); |
| schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); |
| value = Assertions.checkNotNull(atom.readNullTerminatedString()); |
| break; |
| default: |
| Log.w(TAG, "Skipping unsupported emsg version: " + version); |
| return; |
| } |
| |
| byte[] messageData = new byte[atom.bytesLeft()]; |
| atom.readBytes(messageData, /*offset=*/ 0, atom.bytesLeft()); |
| EventMessage eventMessage = new EventMessage(schemeIdUri, value, durationMs, id, messageData); |
| ParsableByteArray encodedEventMessage = |
| new ParsableByteArray(eventMessageEncoder.encode(eventMessage)); |
| int sampleSize = encodedEventMessage.bytesLeft(); |
| |
| // Output the sample data. |
| for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { |
| encodedEventMessage.setPosition(0); |
| emsgTrackOutput.sampleData(encodedEventMessage, sampleSize); |
| } |
| |
| // Output the sample metadata. This is made a little complicated because emsg-v0 atoms |
| // have presentation time *delta* while v1 atoms have absolute presentation time. |
| if (sampleTimeUs == C.TIME_UNSET) { |
| // We need the first sample timestamp in the segment before we can output the metadata. |
| pendingMetadataSampleInfos.addLast( |
| new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize)); |
| pendingMetadataSampleBytes += sampleSize; |
| } else { |
| if (timestampAdjuster != null) { |
| sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); |
| } |
| for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { |
| emsgTrackOutput.sampleMetadata( |
| sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, null); |
| } |
| } |
| } |
| |
| /** Parses a trex atom (defined in 14496-12). */ |
| private static Pair<Integer, DefaultSampleValues> 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 Pair.create(trackId, new DefaultSampleValues(defaultSampleDescriptionIndex, |
| defaultSampleDuration, defaultSampleSize, defaultSampleFlags)); |
| } |
| |
| /** |
| * Parses an mehd atom (defined in 14496-12). |
| */ |
| private static long parseMehd(ParsableByteArray mehd) { |
| mehd.setPosition(Atom.HEADER_SIZE); |
| int fullAtom = mehd.readInt(); |
| int version = Atom.parseFullAtomVersion(fullAtom); |
| return version == 0 ? mehd.readUnsignedInt() : mehd.readUnsignedLongToLong(); |
| } |
| |
| private static void parseMoof(ContainerAtom moof, SparseArray<TrackBundle> trackBundleArray, |
| @Flags 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); |
| // TODO: Support multiple traf boxes per track in a single moof. |
| if (child.type == Atom.TYPE_traf) { |
| parseTraf(child, trackBundleArray, flags, extendedTypeScratch); |
| } |
| } |
| } |
| |
| /** |
| * Parses a traf atom (defined in 14496-12). |
| */ |
| private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray, |
| @Flags int flags, byte[] extendedTypeScratch) throws ParserException { |
| LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); |
| @Nullable TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray); |
| if (trackBundle == null) { |
| return; |
| } |
| |
| TrackFragment fragment = trackBundle.fragment; |
| long decodeTime = fragment.nextFragmentDecodeTime; |
| trackBundle.reset(); |
| |
| @Nullable LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); |
| if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) { |
| decodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data); |
| } |
| |
| parseTruns(traf, trackBundle, decodeTime, flags); |
| |
| @Nullable |
| TrackEncryptionBox encryptionBox = |
| trackBundle.track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); |
| |
| @Nullable LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); |
| if (saiz != null) { |
| parseSaiz(encryptionBox, saiz.data, fragment); |
| } |
| |
| @Nullable LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); |
| if (saio != null) { |
| parseSaio(saio.data, fragment); |
| } |
| |
| @Nullable LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); |
| if (senc != null) { |
| parseSenc(senc.data, fragment); |
| } |
| |
| @Nullable LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp); |
| @Nullable LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd); |
| if (sbgp != null && sgpd != null) { |
| parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null, |
| fragment); |
| } |
| |
| int leafChildrenSize = traf.leafChildren.size(); |
| for (int i = 0; i < leafChildrenSize; i++) { |
| LeafAtom atom = traf.leafChildren.get(i); |
| if (atom.type == Atom.TYPE_uuid) { |
| parseUuid(atom.data, fragment, extendedTypeScratch); |
| } |
| } |
| } |
| |
| private static void parseTruns(ContainerAtom traf, TrackBundle trackBundle, long decodeTime, |
| @Flags int flags) { |
| int trunCount = 0; |
| int totalSampleCount = 0; |
| List<LeafAtom> leafChildren = traf.leafChildren; |
| int leafChildrenSize = leafChildren.size(); |
| for (int i = 0; i < leafChildrenSize; i++) { |
| LeafAtom atom = leafChildren.get(i); |
| if (atom.type == Atom.TYPE_trun) { |
| ParsableByteArray trunData = atom.data; |
| trunData.setPosition(Atom.FULL_HEADER_SIZE); |
| int trunSampleCount = trunData.readUnsignedIntToInt(); |
| if (trunSampleCount > 0) { |
| totalSampleCount += trunSampleCount; |
| trunCount++; |
| } |
| } |
| } |
| trackBundle.currentTrackRunIndex = 0; |
| trackBundle.currentSampleInTrackRun = 0; |
| trackBundle.currentSampleIndex = 0; |
| trackBundle.fragment.initTables(trunCount, totalSampleCount); |
| |
| int trunIndex = 0; |
| int trunStartPosition = 0; |
| for (int i = 0; i < leafChildrenSize; i++) { |
| LeafAtom trun = leafChildren.get(i); |
| if (trun.type == Atom.TYPE_trun) { |
| trunStartPosition = parseTrun(trackBundle, trunIndex++, decodeTime, flags, trun.data, |
| trunStartPosition); |
| } |
| } |
| } |
| |
| private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz, |
| TrackFragment out) throws ParserException { |
| int vectorSize = encryptionBox.perSampleIvSize; |
| saiz.setPosition(Atom.HEADER_SIZE); |
| int fullAtom = saiz.readInt(); |
| int flags = Atom.parseFullAtomFlags(fullAtom); |
| if ((flags & 0x01) == 1) { |
| saiz.skipBytes(8); |
| } |
| int defaultSampleInfoSize = saiz.readUnsignedByte(); |
| |
| int sampleCount = saiz.readUnsignedIntToInt(); |
| if (sampleCount != out.sampleCount) { |
| throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount); |
| } |
| |
| int totalSize = 0; |
| if (defaultSampleInfoSize == 0) { |
| boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable; |
| for (int i = 0; i < sampleCount; i++) { |
| int sampleInfoSize = saiz.readUnsignedByte(); |
| totalSize += sampleInfoSize; |
| sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize; |
| } |
| } else { |
| boolean subsampleEncryption = defaultSampleInfoSize > vectorSize; |
| totalSize += defaultSampleInfoSize * sampleCount; |
| Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); |
| } |
| out.initEncryptionData(totalSize); |
| } |
| |
| /** |
| * Parses a saio atom (defined in 14496-12). |
| * |
| * @param saio The saio atom to decode. |
| * @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); |
| int fullAtom = saio.readInt(); |
| int flags = Atom.parseFullAtomFlags(fullAtom); |
| if ((flags & 0x01) == 1) { |
| saio.skipBytes(8); |
| } |
| |
| int entryCount = saio.readUnsignedIntToInt(); |
| if (entryCount != 1) { |
| // We only support one trun element currently, so always expect one entry. |
| throw new ParserException("Unexpected saio entry count: " + entryCount); |
| } |
| |
| int version = Atom.parseFullAtomVersion(fullAtom); |
| out.auxiliaryDataPosition += |
| version == 0 ? saio.readUnsignedInt() : saio.readUnsignedLongToLong(); |
| } |
| |
| /** |
| * 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 tfhd The tfhd atom to decode. |
| * @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}. |
| */ |
| @Nullable |
| private static TrackBundle parseTfhd( |
| ParsableByteArray tfhd, SparseArray<TrackBundle> trackBundles) { |
| tfhd.setPosition(Atom.HEADER_SIZE); |
| int fullAtom = tfhd.readInt(); |
| int atomFlags = Atom.parseFullAtomFlags(fullAtom); |
| int trackId = tfhd.readInt(); |
| @Nullable TrackBundle trackBundle = getTrackBundle(trackBundles, trackId); |
| if (trackBundle == null) { |
| return null; |
| } |
| if ((atomFlags & 0x01 /* base_data_offset_present */) != 0) { |
| long baseDataPosition = tfhd.readUnsignedLongToLong(); |
| trackBundle.fragment.dataPosition = baseDataPosition; |
| trackBundle.fragment.auxiliaryDataPosition = baseDataPosition; |
| } |
| |
| DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues; |
| int defaultSampleDescriptionIndex = |
| ((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; |
| } |
| |
| private static @Nullable TrackBundle getTrackBundle( |
| SparseArray<TrackBundle> trackBundles, int trackId) { |
| if (trackBundles.size() == 1) { |
| // Ignore track id if there is only one track. This is either because we have a side-loaded |
| // track (flag FLAG_SIDELOADED) or to cope with non-matching track indices (see |
| // https://github.com/google/ExoPlayer/issues/4083). |
| return trackBundles.valueAt(/* index= */ 0); |
| } |
| return trackBundles.get(trackId); |
| } |
| |
| /** |
| * Parses a tfdt atom (defined in 14496-12). |
| * |
| * @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the |
| * media, expressed in the media's timescale. |
| */ |
| private static long parseTfdt(ParsableByteArray tfdt) { |
| tfdt.setPosition(Atom.HEADER_SIZE); |
| int fullAtom = tfdt.readInt(); |
| int version = Atom.parseFullAtomVersion(fullAtom); |
| return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt(); |
| } |
| |
| /** |
| * Parses a trun atom (defined in 14496-12). |
| * |
| * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into |
| * which parsed data should be placed. |
| * @param index Index of the track run in the fragment. |
| * @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 decode. |
| * @return The starting position of samples for the next run. |
| */ |
| private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime, |
| @Flags int flags, ParsableByteArray trun, int trackRunStart) { |
| trun.setPosition(Atom.HEADER_SIZE); |
| int fullAtom = trun.readInt(); |
| int atomFlags = Atom.parseFullAtomFlags(fullAtom); |
| |
| Track track = trackBundle.track; |
| TrackFragment fragment = trackBundle.fragment; |
| DefaultSampleValues defaultSampleValues = fragment.header; |
| |
| fragment.trunLength[index] = trun.readUnsignedIntToInt(); |
| fragment.trunDataPosition[index] = fragment.dataPosition; |
| if ((atomFlags & 0x01 /* data_offset_present */) != 0) { |
| fragment.trunDataPosition[index] += trun.readInt(); |
| } |
| |
| boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0; |
| int firstSampleFlags = defaultSampleValues.flags; |
| if (firstSampleFlagsPresent) { |
| firstSampleFlags = trun.readUnsignedIntToInt(); |
| } |
| |
| 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 = |
| (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. |
| long edtsOffset = 0; |
| |
| // Currently we only support a single edit that moves the entire media timeline (indicated by |
| // duration == 0). Other uses of edit lists are uncommon and unsupported. |
| if (track.editListDurations != null && track.editListDurations.length == 1 |
| && track.editListDurations[0] == 0) { |
| edtsOffset = |
| Util.scaleLargeTimestamp( |
| track.editListMediaTimes[0], C.MILLIS_PER_SECOND, track.timescale); |
| } |
| |
| int[] sampleSizeTable = fragment.sampleSizeTable; |
| int[] sampleCompositionTimeOffsetTable = fragment.sampleCompositionTimeOffsetTable; |
| long[] sampleDecodingTimeTable = fragment.sampleDecodingTimeTable; |
| boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable; |
| |
| boolean workaroundEveryVideoFrameIsSyncFrame = track.type == C.TRACK_TYPE_VIDEO |
| && (flags & FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) != 0; |
| |
| int trackRunEnd = trackRunStart + fragment.trunLength[index]; |
| long timescale = track.timescale; |
| long cumulativeTime = index > 0 ? fragment.nextFragmentDecodeTime : decodeTime; |
| for (int i = trackRunStart; i < trackRunEnd; i++) { |
| // Use trun values if present, otherwise tfhd, otherwise trex. |
| int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt() |
| : defaultSampleValues.duration; |
| int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size; |
| int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags |
| : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags; |
| if (sampleCompositionTimeOffsetsPresent) { |
| // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in |
| // version 0 trun boxes, however a significant number of 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). |
| int sampleOffset = trun.readInt(); |
| sampleCompositionTimeOffsetTable[i] = |
| (int) ((sampleOffset * C.MILLIS_PER_SECOND) / timescale); |
| } else { |
| sampleCompositionTimeOffsetTable[i] = 0; |
| } |
| sampleDecodingTimeTable[i] = |
| Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset; |
| sampleSizeTable[i] = sampleSize; |
| sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 |
| && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); |
| cumulativeTime += sampleDuration; |
| } |
| fragment.nextFragmentDecodeTime = cumulativeTime; |
| return trackRunEnd; |
| } |
| |
| private static void parseUuid(ParsableByteArray uuid, TrackFragment out, |
| byte[] extendedTypeScratch) throws ParserException { |
| uuid.setPosition(Atom.HEADER_SIZE); |
| uuid.readBytes(extendedTypeScratch, 0, 16); |
| |
| // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox. |
| if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) { |
| return; |
| } |
| |
| // Except for the extended type, this box is identical to a SENC box. See "Portable encoding of |
| // audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al, |
| // Section 5.3.2.1." |
| parseSenc(uuid, 16, out); |
| } |
| |
| private static void parseSenc(ParsableByteArray senc, TrackFragment out) throws ParserException { |
| parseSenc(senc, 0, out); |
| } |
| |
| private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) |
| throws ParserException { |
| senc.setPosition(Atom.HEADER_SIZE + offset); |
| int fullAtom = senc.readInt(); |
| int flags = Atom.parseFullAtomFlags(fullAtom); |
| |
| if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) { |
| // TODO: Implement this. |
| throw new ParserException("Overriding TrackEncryptionBox parameters is unsupported."); |
| } |
| |
| boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0; |
| int sampleCount = senc.readUnsignedIntToInt(); |
| if (sampleCount != out.sampleCount) { |
| throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount); |
| } |
| |
| Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); |
| out.initEncryptionData(senc.bytesLeft()); |
| out.fillEncryptionData(senc); |
| } |
| |
| private static void parseSgpd( |
| ParsableByteArray sbgp, |
| ParsableByteArray sgpd, |
| @Nullable String schemeType, |
| TrackFragment out) |
| throws ParserException { |
| sbgp.setPosition(Atom.HEADER_SIZE); |
| int sbgpFullAtom = sbgp.readInt(); |
| if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) { |
| // Only seig grouping type is supported. |
| return; |
| } |
| if (Atom.parseFullAtomVersion(sbgpFullAtom) == 1) { |
| sbgp.skipBytes(4); // default_length. |
| } |
| if (sbgp.readInt() != 1) { // entry_count. |
| throw new ParserException("Entry count in sbgp != 1 (unsupported)."); |
| } |
| |
| sgpd.setPosition(Atom.HEADER_SIZE); |
| int sgpdFullAtom = sgpd.readInt(); |
| if (sgpd.readInt() != SAMPLE_GROUP_TYPE_seig) { |
| // Only seig grouping type is supported. |
| return; |
| } |
| int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom); |
| if (sgpdVersion == 1) { |
| if (sgpd.readUnsignedInt() == 0) { |
| throw new ParserException("Variable length description in sgpd found (unsupported)"); |
| } |
| } else if (sgpdVersion >= 2) { |
| sgpd.skipBytes(4); // default_sample_description_index. |
| } |
| if (sgpd.readUnsignedInt() != 1) { // entry_count. |
| throw new ParserException("Entry count in sgpd != 1 (unsupported)."); |
| } |
| // CencSampleEncryptionInformationGroupEntry |
| sgpd.skipBytes(1); // reserved = 0. |
| int patternByte = sgpd.readUnsignedByte(); |
| int cryptByteBlock = (patternByte & 0xF0) >> 4; |
| int skipByteBlock = patternByte & 0x0F; |
| boolean isProtected = sgpd.readUnsignedByte() == 1; |
| if (!isProtected) { |
| return; |
| } |
| int perSampleIvSize = sgpd.readUnsignedByte(); |
| byte[] keyId = new byte[16]; |
| sgpd.readBytes(keyId, 0, keyId.length); |
| byte[] constantIv = null; |
| if (perSampleIvSize == 0) { |
| int constantIvSize = sgpd.readUnsignedByte(); |
| constantIv = new byte[constantIvSize]; |
| sgpd.readBytes(constantIv, 0, constantIvSize); |
| } |
| out.definesEncryptionData = true; |
| out.trackEncryptionBox = new TrackEncryptionBox(isProtected, schemeType, perSampleIvSize, keyId, |
| cryptByteBlock, skipByteBlock, constantIv); |
| } |
| |
| /** |
| * Parses a sidx atom (defined in 14496-12). |
| * |
| * @param atom The atom data. |
| * @param inputPosition The input position of the first byte after the atom. |
| * @return A pair consisting of the earliest presentation time in microseconds, and the parsed |
| * {@link ChunkIndex}. |
| */ |
| private static Pair<Long, ChunkIndex> parseSidx(ParsableByteArray atom, long inputPosition) |
| throws ParserException { |
| atom.setPosition(Atom.HEADER_SIZE); |
| int fullAtom = atom.readInt(); |
| int version = Atom.parseFullAtomVersion(fullAtom); |
| |
| atom.skipBytes(4); |
| long timescale = atom.readUnsignedInt(); |
| long earliestPresentationTime; |
| long offset = inputPosition; |
| if (version == 0) { |
| earliestPresentationTime = atom.readUnsignedInt(); |
| offset += atom.readUnsignedInt(); |
| } else { |
| earliestPresentationTime = atom.readUnsignedLongToLong(); |
| offset += atom.readUnsignedLongToLong(); |
| } |
| long earliestPresentationTimeUs = Util.scaleLargeTimestamp(earliestPresentationTime, |
| C.MICROS_PER_SECOND, timescale); |
| |
| atom.skipBytes(2); |
| |
| int referenceCount = atom.readUnsignedShort(); |
| int[] sizes = new int[referenceCount]; |
| long[] offsets = new long[referenceCount]; |
| long[] durationsUs = new long[referenceCount]; |
| long[] timesUs = new long[referenceCount]; |
| |
| long time = earliestPresentationTime; |
| long timeUs = earliestPresentationTimeUs; |
| for (int i = 0; i < referenceCount; i++) { |
| int firstInt = atom.readInt(); |
| |
| int type = 0x80000000 & firstInt; |
| if (type != 0) { |
| throw new ParserException("Unhandled indirect reference"); |
| } |
| long referenceDuration = atom.readUnsignedInt(); |
| |
| sizes[i] = 0x7FFFFFFF & firstInt; |
| offsets[i] = offset; |
| |
| // Calculate time and duration values such that any rounding errors are consistent. i.e. That |
| // timesUs[i] + durationsUs[i] == timesUs[i + 1]. |
| timesUs[i] = timeUs; |
| time += referenceDuration; |
| timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale); |
| durationsUs[i] = timeUs - timesUs[i]; |
| |
| atom.skipBytes(4); |
| offset += sizes[i]; |
| } |
| |
| return Pair.create(earliestPresentationTimeUs, |
| new ChunkIndex(sizes, offsets, durationsUs, timesUs)); |
| } |
| |
| private void readEncryptionData(ExtractorInput input) throws IOException { |
| 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); |
| nextTrackBundle.fragment.fillEncryptionData(input); |
| } |
| |
| /** |
| * Attempts to read the next sample in the current mdat atom. The read sample may be output or |
| * skipped. |
| * |
| * <p>If there are no more samples in the current mdat atom then the parser state is transitioned |
| * to {@link #STATE_READING_ATOM_HEADER} and {@code false} is returned. |
| * |
| * <p>It is possible for a sample to be partially read in the case that an exception is thrown. In |
| * this case the method can be called again to read the remainder of the sample. |
| * |
| * @param input The {@link ExtractorInput} from which to read data. |
| * @return Whether a sample was read. The read sample may have been output or skipped. False |
| * indicates that there are no samples left to read in the current mdat. |
| * @throws IOException If an error occurs reading from the input. |
| */ |
| private boolean readSample(ExtractorInput input) throws IOException { |
| if (parserState == STATE_READING_SAMPLE_START) { |
| if (currentTrackBundle == null) { |
| @Nullable TrackBundle 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; |
| } |
| |
| long nextDataPosition = currentTrackBundle.fragment |
| .trunDataPosition[currentTrackBundle.currentTrackRunIndex]; |
| // We skip bytes preceding the next sample to read. |
| int bytesToSkip = (int) (nextDataPosition - input.getPosition()); |
| if (bytesToSkip < 0) { |
| // Assume the sample data must be contiguous in the mdat with no preceding data. |
| Log.w(TAG, "Ignoring negative offset to sample data."); |
| bytesToSkip = 0; |
| } |
| input.skipFully(bytesToSkip); |
| this.currentTrackBundle = currentTrackBundle; |
| } |
| |
| sampleSize = currentTrackBundle.fragment |
| .sampleSizeTable[currentTrackBundle.currentSampleIndex]; |
| |
| if (currentTrackBundle.currentSampleIndex < currentTrackBundle.firstSampleToOutputIndex) { |
| input.skipFully(sampleSize); |
| currentTrackBundle.skipSampleEncryptionData(); |
| if (!currentTrackBundle.next()) { |
| currentTrackBundle = null; |
| } |
| parserState = STATE_READING_SAMPLE_START; |
| return true; |
| } |
| |
| if (currentTrackBundle.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { |
| sampleSize -= Atom.HEADER_SIZE; |
| input.skipFully(Atom.HEADER_SIZE); |
| } |
| |
| if (MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType)) { |
| // AC4 samples need to be prefixed with a clear sample header. |
| sampleBytesWritten = |
| currentTrackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE); |
| Ac4Util.getAc4SampleHeader(sampleSize, scratch); |
| currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); |
| sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; |
| } else { |
| sampleBytesWritten = |
| currentTrackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0); |
| } |
| sampleSize += sampleBytesWritten; |
| parserState = STATE_READING_SAMPLE_CONTINUE; |
| sampleCurrentNalBytesRemaining = 0; |
| } |
| |
| TrackFragment fragment = currentTrackBundle.fragment; |
| Track track = currentTrackBundle.track; |
| TrackOutput output = currentTrackBundle.output; |
| int sampleIndex = currentTrackBundle.currentSampleIndex; |
| long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; |
| if (timestampAdjuster != null) { |
| sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); |
| } |
| if (track.nalUnitLengthFieldLength != 0) { |
| // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case |
| // they're only 1 or 2 bytes long. |
| byte[] nalPrefixData = nalPrefix.data; |
| nalPrefixData[0] = 0; |
| nalPrefixData[1] = 0; |
| nalPrefixData[2] = 0; |
| int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1; |
| int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength; |
| // NAL units are length delimited, but the decoder requires start code delimited units. |
| // Loop until we've written the sample to the track output, replacing length delimiters with |
| // start codes as we encounter them. |
| while (sampleBytesWritten < sampleSize) { |
| if (sampleCurrentNalBytesRemaining == 0) { |
| // Read the NAL length so that we know where we find the next one, and its type. |
| input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength); |
| nalPrefix.setPosition(0); |
| int nalLengthInt = nalPrefix.readInt(); |
| if (nalLengthInt < 1) { |
| throw new ParserException("Invalid NAL length"); |
| } |
| sampleCurrentNalBytesRemaining = nalLengthInt - 1; |
| // Write a start code for the current NAL unit. |
| nalStartCode.setPosition(0); |
| output.sampleData(nalStartCode, 4); |
| // Write the NAL unit type byte. |
| output.sampleData(nalPrefix, 1); |
| processSeiNalUnitPayload = cea608TrackOutputs.length > 0 |
| && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); |
| sampleBytesWritten += 5; |
| sampleSize += nalUnitLengthFieldLengthDiff; |
| } else { |
| int writtenBytes; |
| if (processSeiNalUnitPayload) { |
| // Read and write the payload of the SEI NAL unit. |
| nalBuffer.reset(sampleCurrentNalBytesRemaining); |
| input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining); |
| output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining); |
| writtenBytes = sampleCurrentNalBytesRemaining; |
| // Unescape and process the SEI NAL unit. |
| int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit()); |
| // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte. |
| nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0); |
| nalBuffer.setLimit(unescapedLength); |
| CeaUtil.consume(sampleTimeUs, nalBuffer, cea608TrackOutputs); |
| } else { |
| // Write the payload of the NAL unit. |
| writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); |
| } |
| sampleBytesWritten += writtenBytes; |
| sampleCurrentNalBytesRemaining -= writtenBytes; |
| } |
| } |
| } else { |
| while (sampleBytesWritten < sampleSize) { |
| int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); |
| sampleBytesWritten += writtenBytes; |
| } |
| } |
| |
| @C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex] |
| ? C.BUFFER_FLAG_KEY_FRAME : 0; |
| |
| // Encryption data. |
| TrackOutput.CryptoData cryptoData = null; |
| TrackEncryptionBox encryptionBox = currentTrackBundle.getEncryptionBoxIfEncrypted(); |
| if (encryptionBox != null) { |
| sampleFlags |= C.BUFFER_FLAG_ENCRYPTED; |
| cryptoData = encryptionBox.cryptoData; |
| } |
| |
| output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData); |
| |
| // After we have the sampleTimeUs, we can commit all the pending metadata samples |
| outputPendingMetadataSamples(sampleTimeUs); |
| if (!currentTrackBundle.next()) { |
| currentTrackBundle = null; |
| } |
| parserState = STATE_READING_SAMPLE_START; |
| return true; |
| } |
| |
| private void outputPendingMetadataSamples(long sampleTimeUs) { |
| while (!pendingMetadataSampleInfos.isEmpty()) { |
| MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); |
| pendingMetadataSampleBytes -= sampleInfo.size; |
| long metadataTimeUs = sampleTimeUs + sampleInfo.presentationTimeDeltaUs; |
| if (timestampAdjuster != null) { |
| metadataTimeUs = timestampAdjuster.adjustSampleTimestamp(metadataTimeUs); |
| } |
| for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { |
| emsgTrackOutput.sampleMetadata( |
| metadataTimeUs, |
| C.BUFFER_FLAG_KEY_FRAME, |
| sampleInfo.size, |
| pendingMetadataSampleBytes, |
| null); |
| } |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| @Nullable |
| private static TrackBundle getNextFragmentRun(SparseArray<TrackBundle> 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.currentTrackRunIndex == trackBundle.fragment.trunCount) { |
| // This track fragment contains no more runs in the next mdat box. |
| } else { |
| long trunOffset = trackBundle.fragment.trunDataPosition[trackBundle.currentTrackRunIndex]; |
| if (trunOffset < nextTrackRunOffset) { |
| nextTrackBundle = trackBundle; |
| nextTrackRunOffset = trunOffset; |
| } |
| } |
| } |
| return nextTrackBundle; |
| } |
| |
| /** Returns DrmInitData from leaf atoms. */ |
| @Nullable |
| private static DrmInitData getDrmInitDataFromAtoms(List<Atom.LeafAtom> leafChildren) { |
| @Nullable ArrayList<SchemeData> schemeDatas = null; |
| int leafChildrenSize = leafChildren.size(); |
| for (int i = 0; i < leafChildrenSize; i++) { |
| LeafAtom child = leafChildren.get(i); |
| if (child.type == Atom.TYPE_pssh) { |
| if (schemeDatas == null) { |
| schemeDatas = new ArrayList<>(); |
| } |
| byte[] psshData = child.data.data; |
| @Nullable UUID uuid = PsshAtomUtil.parseUuid(psshData); |
| if (uuid == null) { |
| Log.w(TAG, "Skipped pssh atom (failed to extract uuid)"); |
| } else { |
| schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData)); |
| } |
| } |
| } |
| return schemeDatas == null ? null : new DrmInitData(schemeDatas); |
| } |
| |
| /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */ |
| private static boolean shouldParseLeafAtom(int atom) { |
| return atom == Atom.TYPE_hdlr || atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd |
| || atom == Atom.TYPE_sidx || atom == Atom.TYPE_stsd || atom == Atom.TYPE_tfdt |
| || atom == Atom.TYPE_tfhd || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_trex |
| || atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz |
| || atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid |
| || atom == Atom.TYPE_sbgp || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst |
| || atom == Atom.TYPE_mehd || atom == Atom.TYPE_emsg; |
| } |
| |
| /** Returns whether the extractor should decode a container atom with type {@code atom}. */ |
| private static boolean shouldParseContainerAtom(int atom) { |
| return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia |
| || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_moof |
| || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts; |
| } |
| |
| /** |
| * Holds data corresponding to a metadata sample. |
| */ |
| private static final class MetadataSampleInfo { |
| |
| public final long presentationTimeDeltaUs; |
| public final int size; |
| |
| public MetadataSampleInfo(long presentationTimeDeltaUs, int size) { |
| this.presentationTimeDeltaUs = presentationTimeDeltaUs; |
| this.size = size; |
| } |
| |
| } |
| |
| /** |
| * Holds data corresponding to a single track. |
| */ |
| private static final class TrackBundle { |
| |
| private static final int SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH = 8; |
| |
| public final TrackOutput output; |
| public final TrackFragment fragment; |
| public final ParsableByteArray scratch; |
| |
| public Track track; |
| public DefaultSampleValues defaultSampleValues; |
| public int currentSampleIndex; |
| public int currentSampleInTrackRun; |
| public int currentTrackRunIndex; |
| public int firstSampleToOutputIndex; |
| |
| private final ParsableByteArray encryptionSignalByte; |
| private final ParsableByteArray defaultInitializationVector; |
| |
| public TrackBundle(TrackOutput output) { |
| this.output = output; |
| fragment = new TrackFragment(); |
| scratch = new ParsableByteArray(); |
| encryptionSignalByte = new ParsableByteArray(1); |
| defaultInitializationVector = new ParsableByteArray(); |
| } |
| |
| public void init(Track track, DefaultSampleValues defaultSampleValues) { |
| this.track = Assertions.checkNotNull(track); |
| this.defaultSampleValues = Assertions.checkNotNull(defaultSampleValues); |
| output.format(track.format); |
| reset(); |
| } |
| |
| public void updateDrmInitData(DrmInitData drmInitData) { |
| @Nullable |
| TrackEncryptionBox encryptionBox = |
| track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); |
| @Nullable String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; |
| DrmInitData updatedDrmInitData = drmInitData.copyWithSchemeType(schemeType); |
| Format format = track.format.buildUpon().setDrmInitData(updatedDrmInitData).build(); |
| output.format(format); |
| } |
| |
| /** Resets the current fragment and sample indices. */ |
| public void reset() { |
| fragment.reset(); |
| currentSampleIndex = 0; |
| currentTrackRunIndex = 0; |
| currentSampleInTrackRun = 0; |
| firstSampleToOutputIndex = 0; |
| } |
| |
| /** |
| * Advances {@link #firstSampleToOutputIndex} to point to the sync sample before the specified |
| * seek time in the current fragment. |
| * |
| * @param timeUs The seek time, in microseconds. |
| */ |
| public void seek(long timeUs) { |
| long timeMs = C.usToMs(timeUs); |
| int searchIndex = currentSampleIndex; |
| while (searchIndex < fragment.sampleCount |
| && fragment.getSamplePresentationTime(searchIndex) < timeMs) { |
| if (fragment.sampleIsSyncFrameTable[searchIndex]) { |
| firstSampleToOutputIndex = searchIndex; |
| } |
| searchIndex++; |
| } |
| } |
| |
| /** |
| * Advances the indices in the bundle to point to the next sample in the current fragment. If |
| * the current sample is the last one in the current fragment, then the advanced state will be |
| * {@code currentSampleIndex == fragment.sampleCount}, {@code currentTrackRunIndex == |
| * fragment.trunCount} and {@code #currentSampleInTrackRun == 0}. |
| * |
| * @return Whether the next sample is in the same track run as the previous one. |
| */ |
| public boolean next() { |
| currentSampleIndex++; |
| currentSampleInTrackRun++; |
| if (currentSampleInTrackRun == fragment.trunLength[currentTrackRunIndex]) { |
| currentTrackRunIndex++; |
| currentSampleInTrackRun = 0; |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Outputs the encryption data for the current sample. |
| * |
| * @param sampleSize The size of the current sample in bytes, excluding any additional clear |
| * header that will be prefixed to the sample by the extractor. |
| * @param clearHeaderSize The size of a clear header that will be prefixed to the sample by the |
| * extractor, or 0. |
| * @return The number of written bytes. |
| */ |
| public int outputSampleEncryptionData(int sampleSize, int clearHeaderSize) { |
| TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); |
| if (encryptionBox == null) { |
| return 0; |
| } |
| |
| ParsableByteArray initializationVectorData; |
| int vectorSize; |
| if (encryptionBox.perSampleIvSize != 0) { |
| initializationVectorData = fragment.sampleEncryptionData; |
| vectorSize = encryptionBox.perSampleIvSize; |
| } else { |
| // The default initialization vector should be used. |
| byte[] initVectorData = encryptionBox.defaultInitializationVector; |
| defaultInitializationVector.reset(initVectorData, initVectorData.length); |
| initializationVectorData = defaultInitializationVector; |
| vectorSize = initVectorData.length; |
| } |
| |
| boolean haveSubsampleEncryptionTable = |
| fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); |
| boolean writeSubsampleEncryptionData = haveSubsampleEncryptionTable || clearHeaderSize != 0; |
| |
| // Write the signal byte, containing the vector size and the subsample encryption flag. |
| encryptionSignalByte.data[0] = |
| (byte) (vectorSize | (writeSubsampleEncryptionData ? 0x80 : 0)); |
| encryptionSignalByte.setPosition(0); |
| output.sampleData(encryptionSignalByte, 1); |
| // Write the vector. |
| output.sampleData(initializationVectorData, vectorSize); |
| |
| if (!writeSubsampleEncryptionData) { |
| return 1 + vectorSize; |
| } |
| |
| if (!haveSubsampleEncryptionTable) { |
| // The sample is fully encrypted, except for the additional clear header that the extractor |
| // is going to prefix. We need to synthesize subsample encryption data that takes the header |
| // into account. |
| scratch.reset(SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); |
| // subsampleCount = 1 (unsigned short) |
| scratch.data[0] = (byte) 0; |
| scratch.data[1] = (byte) 1; |
| // clearDataSize = clearHeaderSize (unsigned short) |
| scratch.data[2] = (byte) ((clearHeaderSize >> 8) & 0xFF); |
| scratch.data[3] = (byte) (clearHeaderSize & 0xFF); |
| // encryptedDataSize = sampleSize (unsigned short) |
| scratch.data[4] = (byte) ((sampleSize >> 24) & 0xFF); |
| scratch.data[5] = (byte) ((sampleSize >> 16) & 0xFF); |
| scratch.data[6] = (byte) ((sampleSize >> 8) & 0xFF); |
| scratch.data[7] = (byte) (sampleSize & 0xFF); |
| output.sampleData(scratch, SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); |
| return 1 + vectorSize + SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH; |
| } |
| |
| ParsableByteArray subsampleEncryptionData = fragment.sampleEncryptionData; |
| int subsampleCount = subsampleEncryptionData.readUnsignedShort(); |
| subsampleEncryptionData.skipBytes(-2); |
| int subsampleDataLength = 2 + 6 * subsampleCount; |
| |
| if (clearHeaderSize != 0) { |
| // We need to account for the additional clear header by adding clearHeaderSize to |
| // clearDataSize for the first subsample specified in the subsample encryption data. |
| scratch.reset(subsampleDataLength); |
| scratch.readBytes(subsampleEncryptionData.data, /* offset= */ 0, subsampleDataLength); |
| subsampleEncryptionData.skipBytes(subsampleDataLength); |
| |
| int clearDataSize = (scratch.data[2] & 0xFF) << 8 | (scratch.data[3] & 0xFF); |
| int adjustedClearDataSize = clearDataSize + clearHeaderSize; |
| scratch.data[2] = (byte) ((adjustedClearDataSize >> 8) & 0xFF); |
| scratch.data[3] = (byte) (adjustedClearDataSize & 0xFF); |
| subsampleEncryptionData = scratch; |
| } |
| |
| output.sampleData(subsampleEncryptionData, subsampleDataLength); |
| return 1 + vectorSize + subsampleDataLength; |
| } |
| |
| /** Skips the encryption data for the current sample. */ |
| private void skipSampleEncryptionData() { |
| @Nullable TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); |
| if (encryptionBox == null) { |
| return; |
| } |
| |
| ParsableByteArray sampleEncryptionData = fragment.sampleEncryptionData; |
| if (encryptionBox.perSampleIvSize != 0) { |
| sampleEncryptionData.skipBytes(encryptionBox.perSampleIvSize); |
| } |
| if (fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex)) { |
| sampleEncryptionData.skipBytes(6 * sampleEncryptionData.readUnsignedShort()); |
| } |
| } |
| |
| @Nullable |
| private TrackEncryptionBox getEncryptionBoxIfEncrypted() { |
| int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; |
| @Nullable |
| TrackEncryptionBox encryptionBox = |
| fragment.trackEncryptionBox != null |
| ? fragment.trackEncryptionBox |
| : track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex); |
| return encryptionBox != null && encryptionBox.isEncrypted ? encryptionBox : null; |
| } |
| |
| } |
| |
| } |