summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java538
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java121
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java123
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java308
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java269
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java35
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java62
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java125
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java280
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java48
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java52
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java23
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java336
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java312
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java84
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java131
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java87
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java275
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/PositionHolder.java28
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java141
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java64
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java147
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java129
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java522
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java383
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java131
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java411
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java130
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java308
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java227
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java87
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java141
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java260
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java150
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java57
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java2331
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java114
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/VarintReader.java155
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java46
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java125
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java482
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java60
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java136
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java188
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java558
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java1607
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java32
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java114
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java1660
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java115
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java588
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java824
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java208
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java201
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java148
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java103
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java197
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java108
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java313
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java143
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java114
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java155
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java135
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java57
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java132
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java268
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java198
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java170
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java149
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java209
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java156
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java216
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java332
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java532
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java283
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java181
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java130
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java63
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java333
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java567
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java494
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java116
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java310
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java223
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java109
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java241
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java209
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java259
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java397
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java49
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java134
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java73
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java62
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java140
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java197
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java698
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java232
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java96
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java81
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java562
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java55
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java191
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java74
103 files changed, 26463 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java
new file mode 100644
index 0000000000..b0b7c7da13
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java
@@ -0,0 +1,538 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.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;
+
+/**
+ * A seeker that supports seeking within a stream by searching for the target frame using binary
+ * search.
+ *
+ * <p>This seeker operates on a stream that contains multiple frames (or samples). Each frame is
+ * associated with some kind of timestamps, such as stream time, or frame indices. Given a target
+ * seek time, the seeker will find the corresponding target timestamp, and perform a search
+ * operation within the stream to identify the target frame and return the byte position in the
+ * stream of the target frame.
+ */
+public abstract class BinarySearchSeeker {
+
+ /** A seeker that looks for a given timestamp from an input. */
+ protected interface TimestampSeeker {
+
+ /**
+ * Searches a limited window of the provided input for a target timestamp. The size of the
+ * window is implementation specific, but should be small enough such that it's reasonable for
+ * multiple such reads to occur during a seek operation.
+ *
+ * @param input The {@link ExtractorInput} from which data should be peeked.
+ * @param targetTimestamp The target timestamp.
+ * @return A {@link TimestampSearchResult} that describes the result of the search.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
+ throws IOException, InterruptedException;
+
+ /** Called when a seek operation finishes. */
+ default void onSeekFinished() {}
+ }
+
+ /**
+ * A {@link SeekTimestampConverter} implementation that returns the seek time itself as the
+ * timestamp for a seek time position.
+ */
+ public static final class DefaultSeekTimestampConverter implements SeekTimestampConverter {
+
+ @Override
+ public long timeUsToTargetTime(long timeUs) {
+ return timeUs;
+ }
+ }
+
+ /**
+ * A converter that converts seek time in stream time into target timestamp for the {@link
+ * BinarySearchSeeker}.
+ */
+ protected interface SeekTimestampConverter {
+ /**
+ * Converts a seek time in microseconds into target timestamp for the {@link
+ * BinarySearchSeeker}.
+ */
+ long timeUsToTargetTime(long timeUs);
+ }
+
+ /**
+ * When seeking within the source, if the offset is smaller than or equal to this value, the seek
+ * operation will be performed using a skip operation. Otherwise, the source will be reloaded at
+ * the new seek position.
+ */
+ private static final long MAX_SKIP_BYTES = 256 * 1024;
+
+ protected final BinarySearchSeekMap seekMap;
+ protected final TimestampSeeker timestampSeeker;
+ protected @Nullable SeekOperationParams seekOperationParams;
+
+ private final int minimumSearchRange;
+
+ /**
+ * Constructs an instance.
+ *
+ * @param seekTimestampConverter The {@link SeekTimestampConverter} that converts seek time in
+ * stream time into target timestamp.
+ * @param timestampSeeker A {@link TimestampSeeker} that will be used to search for timestamps
+ * within the stream.
+ * @param durationUs The duration of the stream in microseconds.
+ * @param floorTimePosition The minimum timestamp value (inclusive) in the stream.
+ * @param ceilingTimePosition The minimum timestamp value (exclusive) in the stream.
+ * @param floorBytePosition The starting position of the frame with minimum timestamp value
+ * (inclusive) in the stream.
+ * @param ceilingBytePosition The position after the frame with maximum timestamp value in the
+ * stream.
+ * @param approxBytesPerFrame Approximated bytes per frame.
+ * @param minimumSearchRange The minimum byte range that this binary seeker will operate on. If
+ * the remaining search range is smaller than this value, the search will stop, and the seeker
+ * will return the position at the floor of the range as the result.
+ */
+ @SuppressWarnings("initialization")
+ protected BinarySearchSeeker(
+ SeekTimestampConverter seekTimestampConverter,
+ TimestampSeeker timestampSeeker,
+ long durationUs,
+ long floorTimePosition,
+ long ceilingTimePosition,
+ long floorBytePosition,
+ long ceilingBytePosition,
+ long approxBytesPerFrame,
+ int minimumSearchRange) {
+ this.timestampSeeker = timestampSeeker;
+ this.minimumSearchRange = minimumSearchRange;
+ this.seekMap =
+ new BinarySearchSeekMap(
+ seekTimestampConverter,
+ durationUs,
+ floorTimePosition,
+ ceilingTimePosition,
+ floorBytePosition,
+ ceilingBytePosition,
+ approxBytesPerFrame);
+ }
+
+ /** Returns the seek map for the stream. */
+ public final SeekMap getSeekMap() {
+ return seekMap;
+ }
+
+ /**
+ * Sets the target time in microseconds within the stream to seek to.
+ *
+ * @param timeUs The target time in microseconds within the stream.
+ */
+ public final void setSeekTargetUs(long timeUs) {
+ if (seekOperationParams != null && seekOperationParams.getSeekTimeUs() == timeUs) {
+ return;
+ }
+ seekOperationParams = createSeekParamsForTargetTimeUs(timeUs);
+ }
+
+ /** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */
+ public final boolean isSeeking() {
+ return seekOperationParams != null;
+ }
+
+ /**
+ * Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from
+ * {@link Extractor}.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
+ * to hold the position of the required seek.
+ * @return One of the {@code RESULT_} values defined in {@link Extractor}.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public int handlePendingSeek(ExtractorInput input, PositionHolder seekPositionHolder)
+ throws InterruptedException, IOException {
+ TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker);
+ while (true) {
+ SeekOperationParams seekOperationParams = Assertions.checkNotNull(this.seekOperationParams);
+ long floorPosition = seekOperationParams.getFloorBytePosition();
+ long ceilingPosition = seekOperationParams.getCeilingBytePosition();
+ long searchPosition = seekOperationParams.getNextSearchBytePosition();
+
+ if (ceilingPosition - floorPosition <= minimumSearchRange) {
+ // The seeking range is too small, so we can just continue from the floor position.
+ markSeekOperationFinished(/* foundTargetFrame= */ false, floorPosition);
+ return seekToPosition(input, floorPosition, seekPositionHolder);
+ }
+ if (!skipInputUntilPosition(input, searchPosition)) {
+ return seekToPosition(input, searchPosition, seekPositionHolder);
+ }
+
+ input.resetPeekPosition();
+ TimestampSearchResult timestampSearchResult =
+ timestampSeeker.searchForTimestamp(input, seekOperationParams.getTargetTimePosition());
+
+ switch (timestampSearchResult.type) {
+ case TimestampSearchResult.TYPE_POSITION_OVERESTIMATED:
+ seekOperationParams.updateSeekCeiling(
+ timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
+ break;
+ case TimestampSearchResult.TYPE_POSITION_UNDERESTIMATED:
+ seekOperationParams.updateSeekFloor(
+ timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
+ break;
+ case TimestampSearchResult.TYPE_TARGET_TIMESTAMP_FOUND:
+ markSeekOperationFinished(
+ /* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate);
+ skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate);
+ return seekToPosition(
+ input, timestampSearchResult.bytePositionToUpdate, seekPositionHolder);
+ case TimestampSearchResult.TYPE_NO_TIMESTAMP:
+ // We can't find any timestamp in the search range from the search position.
+ // Give up, and just continue reading from the last search position in this case.
+ markSeekOperationFinished(/* foundTargetFrame= */ false, searchPosition);
+ return seekToPosition(input, searchPosition, seekPositionHolder);
+ default:
+ throw new IllegalStateException("Invalid case");
+ }
+ }
+ }
+
+ protected SeekOperationParams createSeekParamsForTargetTimeUs(long timeUs) {
+ return new SeekOperationParams(
+ timeUs,
+ seekMap.timeUsToTargetTime(timeUs),
+ seekMap.floorTimePosition,
+ seekMap.ceilingTimePosition,
+ seekMap.floorBytePosition,
+ seekMap.ceilingBytePosition,
+ seekMap.approxBytesPerFrame);
+ }
+
+ protected final void markSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
+ seekOperationParams = null;
+ timestampSeeker.onSeekFinished();
+ onSeekOperationFinished(foundTargetFrame, resultPosition);
+ }
+
+ protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
+ // Do nothing.
+ }
+
+ protected final boolean skipInputUntilPosition(ExtractorInput input, long position)
+ throws IOException, InterruptedException {
+ long bytesToSkip = position - input.getPosition();
+ if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) {
+ input.skipFully((int) bytesToSkip);
+ return true;
+ }
+ return false;
+ }
+
+ protected final int seekToPosition(
+ ExtractorInput input, long position, PositionHolder seekPositionHolder) {
+ if (position == input.getPosition()) {
+ return Extractor.RESULT_CONTINUE;
+ } else {
+ seekPositionHolder.position = position;
+ return Extractor.RESULT_SEEK;
+ }
+ }
+
+ /**
+ * Contains parameters for a pending seek operation by {@link BinarySearchSeeker}.
+ *
+ * <p>This class holds parameters for a binary-search for the {@code targetTimePosition} in the
+ * range [floorPosition, ceilingPosition).
+ */
+ protected static class SeekOperationParams {
+ private final long seekTimeUs;
+ private final long targetTimePosition;
+ private final long approxBytesPerFrame;
+
+ private long floorTimePosition;
+ private long ceilingTimePosition;
+ private long floorBytePosition;
+ private long ceilingBytePosition;
+ private long nextSearchBytePosition;
+
+ /**
+ * Returns the next position in the stream to search for target frame, given [floorBytePosition,
+ * ceilingBytePosition), with corresponding [floorTimePosition, ceilingTimePosition).
+ */
+ protected static long calculateNextSearchBytePosition(
+ long targetTimePosition,
+ long floorTimePosition,
+ long ceilingTimePosition,
+ long floorBytePosition,
+ long ceilingBytePosition,
+ long approxBytesPerFrame) {
+ if (floorBytePosition + 1 >= ceilingBytePosition
+ || floorTimePosition + 1 >= ceilingTimePosition) {
+ return floorBytePosition;
+ }
+ long seekTimeDuration = targetTimePosition - floorTimePosition;
+ float estimatedBytesPerTimeUnit =
+ (float) (ceilingBytePosition - floorBytePosition)
+ / (ceilingTimePosition - floorTimePosition);
+ // It's better to under-estimate rather than over-estimate, because the extractor
+ // input can skip forward easily, but cannot rewind easily (it may require a new connection
+ // to be made).
+ // Therefore, we should reduce the estimated position by some amount, so it will converge to
+ // the correct frame earlier.
+ long bytesToSkip = (long) (seekTimeDuration * estimatedBytesPerTimeUnit);
+ long confidenceInterval = bytesToSkip / 20;
+ long estimatedFramePosition = floorBytePosition + bytesToSkip - approxBytesPerFrame;
+ long estimatedPosition = estimatedFramePosition - confidenceInterval;
+ return Util.constrainValue(estimatedPosition, floorBytePosition, ceilingBytePosition - 1);
+ }
+
+ protected SeekOperationParams(
+ long seekTimeUs,
+ long targetTimePosition,
+ long floorTimePosition,
+ long ceilingTimePosition,
+ long floorBytePosition,
+ long ceilingBytePosition,
+ long approxBytesPerFrame) {
+ this.seekTimeUs = seekTimeUs;
+ this.targetTimePosition = targetTimePosition;
+ this.floorTimePosition = floorTimePosition;
+ this.ceilingTimePosition = ceilingTimePosition;
+ this.floorBytePosition = floorBytePosition;
+ this.ceilingBytePosition = ceilingBytePosition;
+ this.approxBytesPerFrame = approxBytesPerFrame;
+ this.nextSearchBytePosition =
+ calculateNextSearchBytePosition(
+ targetTimePosition,
+ floorTimePosition,
+ ceilingTimePosition,
+ floorBytePosition,
+ ceilingBytePosition,
+ approxBytesPerFrame);
+ }
+
+ /**
+ * Returns the floor byte position of the range [floorPosition, ceilingPosition) for this seek
+ * operation.
+ */
+ private long getFloorBytePosition() {
+ return floorBytePosition;
+ }
+
+ /**
+ * Returns the ceiling byte position of the range [floorPosition, ceilingPosition) for this seek
+ * operation.
+ */
+ private long getCeilingBytePosition() {
+ return ceilingBytePosition;
+ }
+
+ /** Returns the target timestamp as translated from the seek time. */
+ private long getTargetTimePosition() {
+ return targetTimePosition;
+ }
+
+ /** Returns the target seek time in microseconds. */
+ private long getSeekTimeUs() {
+ return seekTimeUs;
+ }
+
+ /** Updates the floor constraints (inclusive) of the seek operation. */
+ private void updateSeekFloor(long floorTimePosition, long floorBytePosition) {
+ this.floorTimePosition = floorTimePosition;
+ this.floorBytePosition = floorBytePosition;
+ updateNextSearchBytePosition();
+ }
+
+ /** Updates the ceiling constraints (exclusive) of the seek operation. */
+ private void updateSeekCeiling(long ceilingTimePosition, long ceilingBytePosition) {
+ this.ceilingTimePosition = ceilingTimePosition;
+ this.ceilingBytePosition = ceilingBytePosition;
+ updateNextSearchBytePosition();
+ }
+
+ /** Returns the next position in the stream to search. */
+ private long getNextSearchBytePosition() {
+ return nextSearchBytePosition;
+ }
+
+ private void updateNextSearchBytePosition() {
+ this.nextSearchBytePosition =
+ calculateNextSearchBytePosition(
+ targetTimePosition,
+ floorTimePosition,
+ ceilingTimePosition,
+ floorBytePosition,
+ ceilingBytePosition,
+ approxBytesPerFrame);
+ }
+ }
+
+ /**
+ * Represents possible search results for {@link
+ * TimestampSeeker#searchForTimestamp(ExtractorInput, long)}.
+ */
+ public static final class TimestampSearchResult {
+
+ /** The search found a timestamp that it deems close enough to the given target. */
+ public static final int TYPE_TARGET_TIMESTAMP_FOUND = 0;
+ /** The search found only timestamps larger than the target timestamp. */
+ public static final int TYPE_POSITION_OVERESTIMATED = -1;
+ /** The search found only timestamps smaller than the target timestamp. */
+ public static final int TYPE_POSITION_UNDERESTIMATED = -2;
+ /** The search didn't find any timestamps. */
+ public static final int TYPE_NO_TIMESTAMP = -3;
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ TYPE_TARGET_TIMESTAMP_FOUND,
+ TYPE_POSITION_OVERESTIMATED,
+ TYPE_POSITION_UNDERESTIMATED,
+ TYPE_NO_TIMESTAMP
+ })
+ @interface Type {}
+
+ public static final TimestampSearchResult NO_TIMESTAMP_IN_RANGE_RESULT =
+ new TimestampSearchResult(TYPE_NO_TIMESTAMP, C.TIME_UNSET, C.POSITION_UNSET);
+
+ /** The type of the result. */
+ @Type private final int type;
+
+ /**
+ * When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link
+ * SeekOperationParams#ceilingTimePosition} should be updated with this value. When {@link
+ * #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link
+ * SeekOperationParams#floorTimePosition} should be updated with this value.
+ */
+ private final long timestampToUpdate;
+ /**
+ * When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link
+ * SeekOperationParams#ceilingBytePosition} should be updated with this value. When {@link
+ * #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link
+ * SeekOperationParams#floorBytePosition} should be updated with this value.
+ */
+ private final long bytePositionToUpdate;
+
+ private TimestampSearchResult(
+ @Type int type, long timestampToUpdate, long bytePositionToUpdate) {
+ this.type = type;
+ this.timestampToUpdate = timestampToUpdate;
+ this.bytePositionToUpdate = bytePositionToUpdate;
+ }
+
+ /**
+ * Returns a result to signal that the current position in the input stream overestimates the
+ * true position of the target frame, and the {@link BinarySearchSeeker} should modify its
+ * {@link SeekOperationParams}'s ceiling timestamp and byte position using the given values.
+ */
+ public static TimestampSearchResult overestimatedResult(
+ long newCeilingTimestamp, long newCeilingBytePosition) {
+ return new TimestampSearchResult(
+ TYPE_POSITION_OVERESTIMATED, newCeilingTimestamp, newCeilingBytePosition);
+ }
+
+ /**
+ * Returns a result to signal that the current position in the input stream underestimates the
+ * true position of the target frame, and the {@link BinarySearchSeeker} should modify its
+ * {@link SeekOperationParams}'s floor timestamp and byte position using the given values.
+ */
+ public static TimestampSearchResult underestimatedResult(
+ long newFloorTimestamp, long newCeilingBytePosition) {
+ return new TimestampSearchResult(
+ TYPE_POSITION_UNDERESTIMATED, newFloorTimestamp, newCeilingBytePosition);
+ }
+
+ /**
+ * Returns a result to signal that the target timestamp has been found at {@code
+ * resultBytePosition}, and the seek operation can stop.
+ */
+ public static TimestampSearchResult targetFoundResult(long resultBytePosition) {
+ return new TimestampSearchResult(
+ TYPE_TARGET_TIMESTAMP_FOUND, C.TIME_UNSET, resultBytePosition);
+ }
+ }
+
+ /**
+ * A {@link SeekMap} implementation that returns the estimated byte location from {@link
+ * SeekOperationParams#calculateNextSearchBytePosition(long, long, long, long, long, long)} for
+ * each {@link #getSeekPoints(long)} query.
+ */
+ public static class BinarySearchSeekMap implements SeekMap {
+ private final SeekTimestampConverter seekTimestampConverter;
+ private final long durationUs;
+ private final long floorTimePosition;
+ private final long ceilingTimePosition;
+ private final long floorBytePosition;
+ private final long ceilingBytePosition;
+ private final long approxBytesPerFrame;
+
+ /** Constructs a new instance of this seek map. */
+ public BinarySearchSeekMap(
+ SeekTimestampConverter seekTimestampConverter,
+ long durationUs,
+ long floorTimePosition,
+ long ceilingTimePosition,
+ long floorBytePosition,
+ long ceilingBytePosition,
+ long approxBytesPerFrame) {
+ this.seekTimestampConverter = seekTimestampConverter;
+ this.durationUs = durationUs;
+ this.floorTimePosition = floorTimePosition;
+ this.ceilingTimePosition = ceilingTimePosition;
+ this.floorBytePosition = floorBytePosition;
+ this.ceilingBytePosition = ceilingBytePosition;
+ this.approxBytesPerFrame = approxBytesPerFrame;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ long nextSearchPosition =
+ SeekOperationParams.calculateNextSearchBytePosition(
+ /* targetTimePosition= */ seekTimestampConverter.timeUsToTargetTime(timeUs),
+ /* floorTimePosition= */ floorTimePosition,
+ /* ceilingTimePosition= */ ceilingTimePosition,
+ /* floorBytePosition= */ floorBytePosition,
+ /* ceilingBytePosition= */ ceilingBytePosition,
+ /* approxBytesPerFrame= */ approxBytesPerFrame);
+ return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition));
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /** @see SeekTimestampConverter#timeUsToTargetTime(long) */
+ public long timeUsToTargetTime(long timeUs) {
+ return seekTimestampConverter.timeUsToTargetTime(timeUs);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java
new file mode 100644
index 0000000000..4fdf9f3c55
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java
@@ -0,0 +1,121 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/**
+ * Defines chunks of samples within a media stream.
+ */
+public final class ChunkIndex implements SeekMap {
+
+ /**
+ * The number of chunks.
+ */
+ public final int length;
+
+ /**
+ * The chunk sizes, in bytes.
+ */
+ public final int[] sizes;
+
+ /**
+ * The chunk byte offsets.
+ */
+ public final long[] offsets;
+
+ /**
+ * The chunk durations, in microseconds.
+ */
+ public final long[] durationsUs;
+
+ /**
+ * The start time of each chunk, in microseconds.
+ */
+ public final long[] timesUs;
+
+ private final long durationUs;
+
+ /**
+ * @param sizes The chunk sizes, in bytes.
+ * @param offsets The chunk byte offsets.
+ * @param durationsUs The chunk durations, in microseconds.
+ * @param timesUs The start time of each chunk, in microseconds.
+ */
+ public ChunkIndex(int[] sizes, long[] offsets, long[] durationsUs, long[] timesUs) {
+ this.sizes = sizes;
+ this.offsets = offsets;
+ this.durationsUs = durationsUs;
+ this.timesUs = timesUs;
+ length = sizes.length;
+ if (length > 0) {
+ durationUs = durationsUs[length - 1] + timesUs[length - 1];
+ } else {
+ durationUs = 0;
+ }
+ }
+
+ /**
+ * Obtains the index of the chunk corresponding to a given time.
+ *
+ * @param timeUs The time, in microseconds.
+ * @return The index of the corresponding chunk.
+ */
+ public int getChunkIndex(long timeUs) {
+ return Util.binarySearchFloor(timesUs, timeUs, true, true);
+ }
+
+ // SeekMap implementation.
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ int chunkIndex = getChunkIndex(timeUs);
+ SeekPoint seekPoint = new SeekPoint(timesUs[chunkIndex], offsets[chunkIndex]);
+ if (seekPoint.timeUs >= timeUs || chunkIndex == length - 1) {
+ return new SeekPoints(seekPoint);
+ } else {
+ SeekPoint nextSeekPoint = new SeekPoint(timesUs[chunkIndex + 1], offsets[chunkIndex + 1]);
+ return new SeekPoints(seekPoint, nextSeekPoint);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ChunkIndex("
+ + "length="
+ + length
+ + ", sizes="
+ + Arrays.toString(sizes)
+ + ", offsets="
+ + Arrays.toString(offsets)
+ + ", timeUs="
+ + Arrays.toString(timesUs)
+ + ", durationsUs="
+ + Arrays.toString(durationsUs)
+ + ")";
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java
new file mode 100644
index 0000000000..215aac0e6d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/**
+ * A {@link SeekMap} implementation that assumes the stream has a constant bitrate and consists of
+ * multiple independent frames of the same size. Seek points are calculated to be at frame
+ * boundaries.
+ */
+public class ConstantBitrateSeekMap implements SeekMap {
+
+ private final long inputLength;
+ private final long firstFrameBytePosition;
+ private final int frameSize;
+ private final long dataSize;
+ private final int bitrate;
+ private final long durationUs;
+
+ /**
+ * Constructs a new instance from a stream.
+ *
+ * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
+ * @param firstFrameBytePosition The byte-position of the first frame in the stream.
+ * @param bitrate The bitrate (which is assumed to be constant in the stream).
+ * @param frameSize The size of each frame in the stream in bytes. May be {@link C#LENGTH_UNSET}
+ * if unknown.
+ */
+ public ConstantBitrateSeekMap(
+ long inputLength, long firstFrameBytePosition, int bitrate, int frameSize) {
+ this.inputLength = inputLength;
+ this.firstFrameBytePosition = firstFrameBytePosition;
+ this.frameSize = frameSize == C.LENGTH_UNSET ? 1 : frameSize;
+ this.bitrate = bitrate;
+
+ if (inputLength == C.LENGTH_UNSET) {
+ dataSize = C.LENGTH_UNSET;
+ durationUs = C.TIME_UNSET;
+ } else {
+ dataSize = inputLength - firstFrameBytePosition;
+ durationUs = getTimeUsAtPosition(inputLength, firstFrameBytePosition, bitrate);
+ }
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return dataSize != C.LENGTH_UNSET;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ if (dataSize == C.LENGTH_UNSET) {
+ return new SeekPoints(new SeekPoint(0, firstFrameBytePosition));
+ }
+ long seekFramePosition = getFramePositionForTimeUs(timeUs);
+ long seekTimeUs = getTimeUsAtPosition(seekFramePosition);
+ SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekFramePosition);
+ if (seekTimeUs >= timeUs || seekFramePosition + frameSize >= inputLength) {
+ return new SeekPoints(seekPoint);
+ } else {
+ long secondSeekPosition = seekFramePosition + frameSize;
+ long secondSeekTimeUs = getTimeUsAtPosition(secondSeekPosition);
+ SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
+ return new SeekPoints(seekPoint, secondSeekPoint);
+ }
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /**
+ * Returns the stream time in microseconds for a given position.
+ *
+ * @param position The stream byte-position.
+ * @return The stream time in microseconds for the given position.
+ */
+ public long getTimeUsAtPosition(long position) {
+ return getTimeUsAtPosition(position, firstFrameBytePosition, bitrate);
+ }
+
+ // Internal methods
+
+ /**
+ * Returns the stream time in microseconds for a given stream position.
+ *
+ * @param position The stream byte-position.
+ * @param firstFrameBytePosition The position of the first frame in the stream.
+ * @param bitrate The bitrate (which is assumed to be constant in the stream).
+ * @return The stream time in microseconds for the given stream position.
+ */
+ private static long getTimeUsAtPosition(long position, long firstFrameBytePosition, int bitrate) {
+ return Math.max(0, position - firstFrameBytePosition)
+ * C.BITS_PER_BYTE
+ * C.MICROS_PER_SECOND
+ / bitrate;
+ }
+
+ private long getFramePositionForTimeUs(long timeUs) {
+ long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * C.BITS_PER_BYTE);
+ // Constrain to nearest preceding frame offset.
+ positionOffset = (positionOffset / frameSize) * frameSize;
+ positionOffset =
+ Util.constrainValue(positionOffset, /* min= */ 0, /* max= */ dataSize - frameSize);
+ return firstFrameBytePosition + positionOffset;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java
new file mode 100644
index 0000000000..93009f2d5c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java
@@ -0,0 +1,308 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * An {@link ExtractorInput} that wraps a {@link DataSource}.
+ */
+public final class DefaultExtractorInput implements ExtractorInput {
+
+ private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024;
+ private static final int PEEK_MAX_FREE_SPACE = 512 * 1024;
+ private static final int SCRATCH_SPACE_SIZE = 4096;
+
+ private final byte[] scratchSpace;
+ private final DataSource dataSource;
+ private final long streamLength;
+
+ private long position;
+ private byte[] peekBuffer;
+ private int peekBufferPosition;
+ private int peekBufferLength;
+
+ /**
+ * @param dataSource The wrapped {@link DataSource}.
+ * @param position The initial position in the stream.
+ * @param length The length of the stream, or {@link C#LENGTH_UNSET} if it is unknown.
+ */
+ public DefaultExtractorInput(DataSource dataSource, long position, long length) {
+ this.dataSource = dataSource;
+ this.position = position;
+ this.streamLength = length;
+ peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
+ scratchSpace = new byte[SCRATCH_SPACE_SIZE];
+ }
+
+ @Override
+ public int read(byte[] target, int offset, int length) throws IOException, InterruptedException {
+ int bytesRead = readFromPeekBuffer(target, offset, length);
+ if (bytesRead == 0) {
+ bytesRead =
+ readFromDataSource(
+ target, offset, length, /* bytesAlreadyRead= */ 0, /* allowEndOfInput= */ true);
+ }
+ commitBytesRead(bytesRead);
+ return bytesRead;
+ }
+
+ @Override
+ public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ int bytesRead = readFromPeekBuffer(target, offset, length);
+ while (bytesRead < length && bytesRead != C.RESULT_END_OF_INPUT) {
+ bytesRead = readFromDataSource(target, offset, length, bytesRead, allowEndOfInput);
+ }
+ commitBytesRead(bytesRead);
+ return bytesRead != C.RESULT_END_OF_INPUT;
+ }
+
+ @Override
+ public void readFully(byte[] target, int offset, int length)
+ throws IOException, InterruptedException {
+ readFully(target, offset, length, false);
+ }
+
+ @Override
+ public int skip(int length) throws IOException, InterruptedException {
+ int bytesSkipped = skipFromPeekBuffer(length);
+ if (bytesSkipped == 0) {
+ bytesSkipped =
+ readFromDataSource(scratchSpace, 0, Math.min(length, scratchSpace.length), 0, true);
+ }
+ commitBytesRead(bytesSkipped);
+ return bytesSkipped;
+ }
+
+ @Override
+ public boolean skipFully(int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ int bytesSkipped = skipFromPeekBuffer(length);
+ while (bytesSkipped < length && bytesSkipped != C.RESULT_END_OF_INPUT) {
+ int minLength = Math.min(length, bytesSkipped + scratchSpace.length);
+ bytesSkipped =
+ readFromDataSource(scratchSpace, -bytesSkipped, minLength, bytesSkipped, allowEndOfInput);
+ }
+ commitBytesRead(bytesSkipped);
+ return bytesSkipped != C.RESULT_END_OF_INPUT;
+ }
+
+ @Override
+ public void skipFully(int length) throws IOException, InterruptedException {
+ skipFully(length, false);
+ }
+
+ @Override
+ public int peek(byte[] target, int offset, int length) throws IOException, InterruptedException {
+ ensureSpaceForPeek(length);
+ int peekBufferRemainingBytes = peekBufferLength - peekBufferPosition;
+ int bytesPeeked;
+ if (peekBufferRemainingBytes == 0) {
+ bytesPeeked =
+ readFromDataSource(
+ peekBuffer,
+ peekBufferPosition,
+ length,
+ /* bytesAlreadyRead= */ 0,
+ /* allowEndOfInput= */ true);
+ if (bytesPeeked == C.RESULT_END_OF_INPUT) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ peekBufferLength += bytesPeeked;
+ } else {
+ bytesPeeked = Math.min(length, peekBufferRemainingBytes);
+ }
+ System.arraycopy(peekBuffer, peekBufferPosition, target, offset, bytesPeeked);
+ peekBufferPosition += bytesPeeked;
+ return bytesPeeked;
+ }
+
+ @Override
+ public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ if (!advancePeekPosition(length, allowEndOfInput)) {
+ return false;
+ }
+ System.arraycopy(peekBuffer, peekBufferPosition - length, target, offset, length);
+ return true;
+ }
+
+ @Override
+ public void peekFully(byte[] target, int offset, int length)
+ throws IOException, InterruptedException {
+ peekFully(target, offset, length, false);
+ }
+
+ @Override
+ public boolean advancePeekPosition(int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ ensureSpaceForPeek(length);
+ int bytesPeeked = peekBufferLength - peekBufferPosition;
+ while (bytesPeeked < length) {
+ bytesPeeked = readFromDataSource(peekBuffer, peekBufferPosition, length, bytesPeeked,
+ allowEndOfInput);
+ if (bytesPeeked == C.RESULT_END_OF_INPUT) {
+ return false;
+ }
+ peekBufferLength = peekBufferPosition + bytesPeeked;
+ }
+ peekBufferPosition += length;
+ return true;
+ }
+
+ @Override
+ public void advancePeekPosition(int length) throws IOException, InterruptedException {
+ advancePeekPosition(length, false);
+ }
+
+ @Override
+ public void resetPeekPosition() {
+ peekBufferPosition = 0;
+ }
+
+ @Override
+ public long getPeekPosition() {
+ return position + peekBufferPosition;
+ }
+
+ @Override
+ public long getPosition() {
+ return position;
+ }
+
+ @Override
+ public long getLength() {
+ return streamLength;
+ }
+
+ @Override
+ public <E extends Throwable> void setRetryPosition(long position, E e) throws E {
+ Assertions.checkArgument(position >= 0);
+ this.position = position;
+ throw e;
+ }
+
+ /**
+ * Ensures {@code peekBuffer} is large enough to store at least {@code length} bytes from the
+ * current peek position.
+ */
+ private void ensureSpaceForPeek(int length) {
+ int requiredLength = peekBufferPosition + length;
+ if (requiredLength > peekBuffer.length) {
+ int newPeekCapacity = Util.constrainValue(peekBuffer.length * 2,
+ requiredLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE, requiredLength + PEEK_MAX_FREE_SPACE);
+ peekBuffer = Arrays.copyOf(peekBuffer, newPeekCapacity);
+ }
+ }
+
+ /**
+ * Skips from the peek buffer.
+ *
+ * @param length The maximum number of bytes to skip from the peek buffer.
+ * @return The number of bytes skipped.
+ */
+ private int skipFromPeekBuffer(int length) {
+ int bytesSkipped = Math.min(peekBufferLength, length);
+ updatePeekBuffer(bytesSkipped);
+ return bytesSkipped;
+ }
+
+ /**
+ * Reads from the peek buffer.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The maximum number of bytes to read from the peek buffer.
+ * @return The number of bytes read.
+ */
+ private int readFromPeekBuffer(byte[] target, int offset, int length) {
+ if (peekBufferLength == 0) {
+ return 0;
+ }
+ int peekBytes = Math.min(peekBufferLength, length);
+ System.arraycopy(peekBuffer, 0, target, offset, peekBytes);
+ updatePeekBuffer(peekBytes);
+ return peekBytes;
+ }
+
+ /**
+ * Updates the peek buffer's length, position and contents after consuming data.
+ *
+ * @param bytesConsumed The number of bytes consumed from the peek buffer.
+ */
+ private void updatePeekBuffer(int bytesConsumed) {
+ peekBufferLength -= bytesConsumed;
+ peekBufferPosition = 0;
+ byte[] newPeekBuffer = peekBuffer;
+ if (peekBufferLength < peekBuffer.length - PEEK_MAX_FREE_SPACE) {
+ newPeekBuffer = new byte[peekBufferLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
+ }
+ System.arraycopy(peekBuffer, bytesConsumed, newPeekBuffer, 0, peekBufferLength);
+ peekBuffer = newPeekBuffer;
+ }
+
+ /**
+ * Starts or continues a read from the data source.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The maximum number of bytes to read from the input.
+ * @param bytesAlreadyRead The number of bytes already read from the input.
+ * @param allowEndOfInput True if encountering the end of the input having read no data is
+ * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it
+ * should be considered an error, causing an {@link EOFException} to be thrown.
+ * @return The total number of bytes read so far, or {@link C#RESULT_END_OF_INPUT} if
+ * {@code allowEndOfInput} is true and the input has ended having read no bytes.
+ * @throws EOFException If the end of input was encountered having partially satisfied the read
+ * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were
+ * read and {@code allowEndOfInput} is false.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private int readFromDataSource(byte[] target, int offset, int length, int bytesAlreadyRead,
+ boolean allowEndOfInput) throws InterruptedException, IOException {
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+ int bytesRead = dataSource.read(target, offset + bytesAlreadyRead, length - bytesAlreadyRead);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ if (bytesAlreadyRead == 0 && allowEndOfInput) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ throw new EOFException();
+ }
+ return bytesAlreadyRead + bytesRead;
+ }
+
+ /**
+ * Advances the position by the specified number of bytes read.
+ *
+ * @param bytesRead The number of bytes read.
+ */
+ private void commitBytesRead(int bytesRead) {
+ if (bytesRead != C.RESULT_END_OF_INPUT) {
+ position += bytesRead;
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
new file mode 100644
index 0000000000..8425f89860
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
@@ -0,0 +1,269 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.amr.AmrExtractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flac.FlacExtractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv.FlvExtractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg.OggExtractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac4Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.PsExtractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsExtractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav.WavExtractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.lang.reflect.Constructor;
+
+/**
+ * An {@link ExtractorsFactory} that provides an array of extractors for the following formats:
+ *
+ * <ul>
+ * <li>MP4, including M4A ({@link Mp4Extractor})
+ * <li>fMP4 ({@link FragmentedMp4Extractor})
+ * <li>Matroska and WebM ({@link MatroskaExtractor})
+ * <li>Ogg Vorbis/FLAC ({@link OggExtractor}
+ * <li>MP3 ({@link Mp3Extractor})
+ * <li>AAC ({@link AdtsExtractor})
+ * <li>MPEG TS ({@link TsExtractor})
+ * <li>MPEG PS ({@link PsExtractor})
+ * <li>FLV ({@link FlvExtractor})
+ * <li>WAV ({@link WavExtractor})
+ * <li>AC3 ({@link Ac3Extractor})
+ * <li>AC4 ({@link Ac4Extractor})
+ * <li>AMR ({@link AmrExtractor})
+ * <li>FLAC
+ * <ul>
+ * <li>If available, the FLAC extension extractor is used.
+ * <li>Otherwise, the core {@link FlacExtractor} is used. Note that Android devices do not
+ * generally include a FLAC decoder before API 27. This can be worked around by using
+ * the FLAC extension or the FFmpeg extension.
+ * </ul>
+ * </ul>
+ */
+public final class DefaultExtractorsFactory implements ExtractorsFactory {
+
+ private static final Constructor<? extends Extractor> FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR;
+
+ static {
+ Constructor<? extends Extractor> flacExtensionExtractorConstructor = null;
+ try {
+ // LINT.IfChange
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ boolean isFlacNativeLibraryAvailable =
+ Boolean.TRUE.equals(
+ Class.forName("com.google.android.exoplayer2.ext.flac.FlacLibrary")
+ .getMethod("isAvailable")
+ .invoke(/* obj= */ null));
+ if (isFlacNativeLibraryAvailable) {
+ flacExtensionExtractorConstructor =
+ Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor")
+ .asSubclass(Extractor.class)
+ .getConstructor();
+ }
+ // LINT.ThenChange(../../../../../../../../proguard-rules.txt)
+ } catch (ClassNotFoundException e) {
+ // Expected if the app was built without the FLAC extension.
+ } catch (Exception e) {
+ // The FLAC extension is present, but instantiation failed.
+ throw new RuntimeException("Error instantiating FLAC extension", e);
+ }
+ FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR = flacExtensionExtractorConstructor;
+ }
+
+ private boolean constantBitrateSeekingEnabled;
+ private @AdtsExtractor.Flags int adtsFlags;
+ private @AmrExtractor.Flags int amrFlags;
+ private @MatroskaExtractor.Flags int matroskaFlags;
+ private @Mp4Extractor.Flags int mp4Flags;
+ private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags;
+ private @Mp3Extractor.Flags int mp3Flags;
+ private @TsExtractor.Mode int tsMode;
+ private @DefaultTsPayloadReaderFactory.Flags int tsFlags;
+
+ public DefaultExtractorsFactory() {
+ tsMode = TsExtractor.MODE_SINGLE_PMT;
+ }
+
+ /**
+ * Convenience method to set whether approximate seeking using constant bitrate assumptions should
+ * be enabled for all extractors that support it. If set to true, the flags required to enable
+ * this functionality will be OR'd with those passed to the setters when creating extractor
+ * instances. If set to false then the flags passed to the setters will be used without
+ * modification.
+ *
+ * @param constantBitrateSeekingEnabled Whether approximate seeking using a constant bitrate
+ * assumption should be enabled for all extractors that support it.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setConstantBitrateSeekingEnabled(
+ boolean constantBitrateSeekingEnabled) {
+ this.constantBitrateSeekingEnabled = constantBitrateSeekingEnabled;
+ return this;
+ }
+
+ /**
+ * Sets flags for {@link AdtsExtractor} instances created by the factory.
+ *
+ * @see AdtsExtractor#AdtsExtractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setAdtsExtractorFlags(
+ @AdtsExtractor.Flags int flags) {
+ this.adtsFlags = flags;
+ return this;
+ }
+
+ /**
+ * Sets flags for {@link AmrExtractor} instances created by the factory.
+ *
+ * @see AmrExtractor#AmrExtractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setAmrExtractorFlags(@AmrExtractor.Flags int flags) {
+ this.amrFlags = flags;
+ return this;
+ }
+
+ /**
+ * Sets flags for {@link MatroskaExtractor} instances created by the factory.
+ *
+ * @see MatroskaExtractor#MatroskaExtractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setMatroskaExtractorFlags(
+ @MatroskaExtractor.Flags int flags) {
+ this.matroskaFlags = flags;
+ return this;
+ }
+
+ /**
+ * Sets flags for {@link Mp4Extractor} instances created by the factory.
+ *
+ * @see Mp4Extractor#Mp4Extractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setMp4ExtractorFlags(@Mp4Extractor.Flags int flags) {
+ this.mp4Flags = flags;
+ return this;
+ }
+
+ /**
+ * Sets flags for {@link FragmentedMp4Extractor} instances created by the factory.
+ *
+ * @see FragmentedMp4Extractor#FragmentedMp4Extractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setFragmentedMp4ExtractorFlags(
+ @FragmentedMp4Extractor.Flags int flags) {
+ this.fragmentedMp4Flags = flags;
+ return this;
+ }
+
+ /**
+ * Sets flags for {@link Mp3Extractor} instances created by the factory.
+ *
+ * @see Mp3Extractor#Mp3Extractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setMp3ExtractorFlags(@Mp3Extractor.Flags int flags) {
+ mp3Flags = flags;
+ return this;
+ }
+
+ /**
+ * Sets the mode for {@link TsExtractor} instances created by the factory.
+ *
+ * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory)
+ * @param mode The mode to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setTsExtractorMode(@TsExtractor.Mode int mode) {
+ tsMode = mode;
+ return this;
+ }
+
+ /**
+ * Sets flags for {@link DefaultTsPayloadReaderFactory}s used by {@link TsExtractor} instances
+ * created by the factory.
+ *
+ * @see TsExtractor#TsExtractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setTsExtractorFlags(
+ @DefaultTsPayloadReaderFactory.Flags int flags) {
+ tsFlags = flags;
+ return this;
+ }
+
+ @Override
+ public synchronized Extractor[] createExtractors() {
+ Extractor[] extractors = new Extractor[14];
+ extractors[0] = new MatroskaExtractor(matroskaFlags);
+ extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags);
+ extractors[2] = new Mp4Extractor(mp4Flags);
+ extractors[3] =
+ new Mp3Extractor(
+ mp3Flags
+ | (constantBitrateSeekingEnabled
+ ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
+ : 0));
+ extractors[4] =
+ new AdtsExtractor(
+ adtsFlags
+ | (constantBitrateSeekingEnabled
+ ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
+ : 0));
+ extractors[5] = new Ac3Extractor();
+ extractors[6] = new TsExtractor(tsMode, tsFlags);
+ extractors[7] = new FlvExtractor();
+ extractors[8] = new OggExtractor();
+ extractors[9] = new PsExtractor();
+ extractors[10] = new WavExtractor();
+ extractors[11] =
+ new AmrExtractor(
+ amrFlags
+ | (constantBitrateSeekingEnabled
+ ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
+ : 0));
+ extractors[12] = new Ac4Extractor();
+ if (FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR != null) {
+ try {
+ extractors[13] = FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance();
+ } catch (Exception e) {
+ // Should never happen.
+ throw new IllegalStateException("Unexpected error creating FLAC extractor", e);
+ }
+ } else {
+ extractors[13] = new FlacExtractor();
+ }
+ return extractors;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java
new file mode 100644
index 0000000000..06c90ae874
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+/** A dummy {@link ExtractorOutput} implementation. */
+public final class DummyExtractorOutput implements ExtractorOutput {
+
+ @Override
+ public TrackOutput track(int id, int type) {
+ return new DummyTrackOutput();
+ }
+
+ @Override
+ public void endTracks() {
+ // Do nothing.
+ }
+
+ @Override
+ public void seekMap(SeekMap seekMap) {
+ // Do nothing.
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
new file mode 100644
index 0000000000..6df947731d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
@@ -0,0 +1,62 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * A dummy {@link TrackOutput} implementation.
+ */
+public final class DummyTrackOutput implements TrackOutput {
+
+ @Override
+ public void format(Format format) {
+ // Do nothing.
+ }
+
+ @Override
+ public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ int bytesSkipped = input.skip(length);
+ if (bytesSkipped == C.RESULT_END_OF_INPUT) {
+ if (allowEndOfInput) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ throw new EOFException();
+ }
+ return bytesSkipped;
+ }
+
+ @Override
+ public void sampleData(ParsableByteArray data, int length) {
+ data.skipBytes(length);
+ }
+
+ @Override
+ public void sampleMetadata(
+ long timeUs,
+ @C.BufferFlags int flags,
+ int size,
+ int offset,
+ @Nullable CryptoData cryptoData) {
+ // Do nothing.
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java
new file mode 100644
index 0000000000..aeb7028c3f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java
@@ -0,0 +1,125 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Extracts media data from a container format.
+ */
+public interface Extractor {
+
+ /**
+ * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed
+ * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data
+ * continuing from the position in the stream reached by the returning call.
+ */
+ int RESULT_CONTINUE = 0;
+ /**
+ * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed
+ * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting
+ * from a specified position in the stream.
+ */
+ int RESULT_SEEK = 1;
+ /**
+ * Returned by {@link #read(ExtractorInput, PositionHolder)} if the end of the
+ * {@link ExtractorInput} was reached. Equal to {@link C#RESULT_END_OF_INPUT}.
+ */
+ int RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT;
+
+ /**
+ * Result values that can be returned by {@link #read(ExtractorInput, PositionHolder)}. One of
+ * {@link #RESULT_CONTINUE}, {@link #RESULT_SEEK} or {@link #RESULT_END_OF_INPUT}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {RESULT_CONTINUE, RESULT_SEEK, RESULT_END_OF_INPUT})
+ @interface ReadResult {}
+
+ /**
+ * Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must
+ * provide data from the start of the stream.
+ * <p>
+ * If {@code true} is returned, the {@code input}'s reading position may have been modified.
+ * Otherwise, only its peek position may have been modified.
+ *
+ * @param input The {@link ExtractorInput} from which data should be peeked/read.
+ * @return Whether this extractor can read the provided input.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ boolean sniff(ExtractorInput input) throws IOException, InterruptedException;
+
+ /**
+ * Initializes the extractor with an {@link ExtractorOutput}. Called at most once.
+ *
+ * @param output An {@link ExtractorOutput} to receive extracted data.
+ */
+ void init(ExtractorOutput output);
+
+ /**
+ * Extracts data read from a provided {@link ExtractorInput}. Must not be called before {@link
+ * #init(ExtractorOutput)}.
+ *
+ * <p>A single call to this method will block until some progress has been made, but will not
+ * block for longer than this. Hence each call will consume only a small amount of input data.
+ *
+ * <p>In the common case, {@link #RESULT_CONTINUE} is returned to indicate that the {@link
+ * ExtractorInput} passed to the next read is required to provide data continuing from the
+ * position in the stream reached by the returning call. If the extractor requires data to be
+ * provided from a different position, then that position is set in {@code seekPosition} and
+ * {@link #RESULT_SEEK} is returned. If the extractor reached the end of the data provided by the
+ * {@link ExtractorInput}, then {@link #RESULT_END_OF_INPUT} is returned.
+ *
+ * <p>When this method throws an {@link IOException} or an {@link InterruptedException},
+ * extraction may continue by providing an {@link ExtractorInput} with an unchanged {@link
+ * ExtractorInput#getPosition() read position} to a subsequent call to this method.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @param seekPosition If {@link #RESULT_SEEK} is returned, this holder is updated to hold the
+ * position of the required data.
+ * @return One of the {@code RESULT_} values defined in this interface.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ @ReadResult
+ int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException;
+
+ /**
+ * Notifies the extractor that a seek has occurred.
+ * <p>
+ * Following a call to this method, the {@link ExtractorInput} passed to the next invocation of
+ * {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting from {@code
+ * position} in the stream. Valid random access positions are the start of the stream and
+ * positions that can be obtained from any {@link SeekMap} passed to the {@link ExtractorOutput}.
+ *
+ * @param position The byte offset in the stream from which data will be provided.
+ * @param timeUs The seek time in microseconds.
+ */
+ void seek(long position, long timeUs);
+
+ /**
+ * Releases all kept resources.
+ */
+ void release();
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java
new file mode 100644
index 0000000000..351df1e79e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java
@@ -0,0 +1,280 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Provides data to be consumed by an {@link Extractor}.
+ *
+ * <p>This interface provides two modes of accessing the underlying input. See the subheadings below
+ * for more info about each mode.
+ *
+ * <ul>
+ * <li>The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like
+ * byte-level access operations.
+ * <li>The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user
+ * wants to read an entire block/frame/header of known length.
+ * </ul>
+ *
+ * <h3>{@link InputStream}-like methods</h3>
+ *
+ * <p>The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like
+ * byte-level access operations. The {@code length} parameter is a maximum, and each method returns
+ * the number of bytes actually processed. This may be less than {@code length} because the end of
+ * the input was reached, or the method was interrupted, or the operation was aborted early for
+ * another reason.
+ *
+ * <h3>Block-based methods</h3>
+ *
+ * <p>The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user
+ * wants to read an entire block/frame/header of known length.
+ *
+ * <p>These methods all have a variant that takes a boolean {@code allowEndOfInput} parameter. This
+ * parameter is intended to be set to true when the caller believes the input might be fully
+ * exhausted before the call is made (i.e. they've previously read/skipped/peeked the final
+ * block/frame/header). It's <b>not</b> intended to allow a partial read (i.e. greater than 0 bytes,
+ * but less than {@code length}) to succeed - this will always throw an {@link EOFException} from
+ * these methods (a partial read is assumed to indicate a malformed block/frame/header - and
+ * therefore a malformed file).
+ *
+ * <p>The expected behaviour of the block-based methods is therefore:
+ *
+ * <ul>
+ * <li>Already at end-of-input and {@code allowEndOfInput=false}: Throw {@link EOFException}.
+ * <li>Already at end-of-input and {@code allowEndOfInput=true}: Return {@code false}.
+ * <li>Encounter end-of-input during read/skip/peek/advance: Throw {@link EOFException}
+ * (regardless of {@code allowEndOfInput}).
+ * </ul>
+ */
+public interface ExtractorInput {
+
+ /**
+ * Reads up to {@code length} bytes from the input and resets the peek position.
+ * <p>
+ * This method blocks until at least one byte of data can be read, the end of the input is
+ * detected, or an exception is thrown.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The maximum number of bytes to read from the input.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the input has ended.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ int read(byte[] target, int offset, int length) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #read(byte[], int, int)}, but reads the requested {@code length} in full.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The number of bytes to read from the input.
+ * @param allowEndOfInput True if encountering the end of the input having read no data is
+ * allowed, and should result in {@code false} being returned. False if it should be
+ * considered an error, causing an {@link EOFException} to be thrown. See note in class
+ * Javadoc.
+ * @return True if the read was successful. False if {@code allowEndOfInput=true} and the end of
+ * the input was encountered having read no data.
+ * @throws EOFException If the end of input was encountered having partially satisfied the read
+ * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were
+ * read and {@code allowEndOfInput} is false.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException;
+
+ /**
+ * Equivalent to {@link #readFully(byte[], int, int, boolean) readFully(target, offset, length,
+ * false)}.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The number of bytes to read from the input.
+ * @throws EOFException If the end of input was encountered.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void readFully(byte[] target, int offset, int length) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #read(byte[], int, int)}, except the data is skipped instead of read.
+ *
+ * @param length The maximum number of bytes to skip from the input.
+ * @return The number of bytes skipped, or {@link C#RESULT_END_OF_INPUT} if the input has ended.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ int skip(int length) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #readFully(byte[], int, int, boolean)}, except the data is skipped instead of read.
+ *
+ * @param length The number of bytes to skip from the input.
+ * @param allowEndOfInput True if encountering the end of the input having skipped no data is
+ * allowed, and should result in {@code false} being returned. False if it should be
+ * considered an error, causing an {@link EOFException} to be thrown. See note in class
+ * Javadoc.
+ * @return True if the skip was successful. False if {@code allowEndOfInput=true} and the end of
+ * the input was encountered having skipped no data.
+ * @throws EOFException If the end of input was encountered having partially satisfied the skip
+ * (i.e. having skipped at least one byte, but fewer than {@code length}), or if no bytes were
+ * skipped and {@code allowEndOfInput} is false.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ boolean skipFully(int length, boolean allowEndOfInput) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #readFully(byte[], int, int)}, except the data is skipped instead of read.
+ * <p>
+ * Encountering the end of input is always considered an error, and will result in an
+ * {@link EOFException} being thrown.
+ *
+ * @param length The number of bytes to skip from the input.
+ * @throws EOFException If the end of input was encountered.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void skipFully(int length) throws IOException, InterruptedException;
+
+ /**
+ * Peeks up to {@code length} bytes from the peek position. The current read position is left
+ * unchanged.
+ *
+ * <p>This method blocks until at least one byte of data can be peeked, the end of the input is
+ * detected, or an exception is thrown.
+ *
+ * <p>Calling {@link #resetPeekPosition()} resets the peek position to equal the current read
+ * position, so the caller can peek the same data again. Reading or skipping also resets the peek
+ * position.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The maximum number of bytes to peek from the input.
+ * @return The number of bytes peeked, or {@link C#RESULT_END_OF_INPUT} if the input has ended.
+ * @throws IOException If an error occurs peeking from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ int peek(byte[] target, int offset, int length) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #peek(byte[], int, int)}, but peeks the requested {@code length} in full.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The number of bytes to peek from the input.
+ * @param allowEndOfInput True if encountering the end of the input having peeked no data is
+ * allowed, and should result in {@code false} being returned. False if it should be
+ * considered an error, causing an {@link EOFException} to be thrown. See note in class
+ * Javadoc.
+ * @return True if the peek was successful. False if {@code allowEndOfInput=true} and the end of
+ * the input was encountered having peeked no data.
+ * @throws EOFException If the end of input was encountered having partially satisfied the peek
+ * (i.e. having peeked at least one byte, but fewer than {@code length}), or if no bytes were
+ * peeked and {@code allowEndOfInput} is false.
+ * @throws IOException If an error occurs peeking from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException;
+
+ /**
+ * Equivalent to {@link #peekFully(byte[], int, int, boolean) peekFully(target, offset, length,
+ * false)}.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The number of bytes to peek from the input.
+ * @throws EOFException If the end of input was encountered.
+ * @throws IOException If an error occurs peeking from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException;
+
+ /**
+ * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int,
+ * boolean)} except the data is skipped instead of read.
+ *
+ * @param length The number of bytes by which to advance the peek position.
+ * @param allowEndOfInput True if encountering the end of the input before advancing is allowed,
+ * and should result in {@code false} being returned. False if it should be considered an
+ * error, causing an {@link EOFException} to be thrown. See note in class Javadoc.
+ * @return True if advancing the peek position was successful. False if {@code
+ * allowEndOfInput=true} and the end of the input was encountered before advancing over any
+ * data.
+ * @throws EOFException If the end of input was encountered having partially advanced (i.e. having
+ * advanced by at least one byte, but fewer than {@code length}), or if the end of input was
+ * encountered before advancing and {@code allowEndOfInput} is false.
+ * @throws IOException If an error occurs advancing the peek position.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ boolean advancePeekPosition(int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException;
+
+ /**
+ * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int)}
+ * except the data is skipped instead of read.
+ *
+ * @param length The number of bytes to peek from the input.
+ * @throws EOFException If the end of input was encountered.
+ * @throws IOException If an error occurs peeking from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void advancePeekPosition(int length) throws IOException, InterruptedException;
+
+ /**
+ * Resets the peek position to equal the current read position.
+ */
+ void resetPeekPosition();
+
+ /**
+ * Returns the current peek position (byte offset) in the stream.
+ *
+ * @return The peek position (byte offset) in the stream.
+ */
+ long getPeekPosition();
+
+ /**
+ * Returns the current read position (byte offset) in the stream.
+ *
+ * @return The read position (byte offset) in the stream.
+ */
+ long getPosition();
+
+ /**
+ * Returns the length of the source stream, or {@link C#LENGTH_UNSET} if it is unknown.
+ *
+ * @return The length of the source stream, or {@link C#LENGTH_UNSET}.
+ */
+ long getLength();
+
+ /**
+ * Called when reading fails and the required retry position is different from the last position.
+ * After setting the retry position it throws the given {@link Throwable}.
+ *
+ * @param <E> Type of {@link Throwable} to be thrown.
+ * @param position The required retry position.
+ * @param e {@link Throwable} to be thrown.
+ * @throws E The given {@link Throwable} object.
+ */
+ <E extends Throwable> void setRetryPosition(long position, E e) throws E;
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java
new file mode 100644
index 0000000000..8708758265
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java
@@ -0,0 +1,48 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+/**
+ * Receives stream level data extracted by an {@link Extractor}.
+ */
+public interface ExtractorOutput {
+
+ /**
+ * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track.
+ * <p>
+ * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}.
+ *
+ * @param id A track identifier.
+ * @param type The type of the track. Typically one of the {@link org.mozilla.thirdparty.com.google.android.exoplayer2C}
+ * {@code TRACK_TYPE_*} constants.
+ * @return The {@link TrackOutput} for the given track identifier.
+ */
+ TrackOutput track(int id, int type);
+
+ /**
+ * Called when all tracks have been identified, meaning no new {@code trackId} values will be
+ * passed to {@link #track(int, int)}.
+ */
+ void endTracks();
+
+ /**
+ * Called when a {@link SeekMap} has been extracted from the stream.
+ *
+ * @param seekMap The extracted {@link SeekMap}.
+ */
+ void seekMap(SeekMap seekMap);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java
new file mode 100644
index 0000000000..6951f7e311
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.io.IOException;
+
+/** Extractor related utility methods. */
+/* package */ final class ExtractorUtil {
+
+ /**
+ * Peeks {@code length} bytes from the input peek position, or all the bytes to the end of the
+ * input if there was less than {@code length} bytes left.
+ *
+ * <p>If an exception is thrown, there is no guarantee on the peek position.
+ *
+ * @param input The stream input to peek the data from.
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The maximum number of bytes to peek from the input.
+ * @return The number of bytes peeked.
+ * @throws IOException If an error occurs peeking from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ public static int peekToLength(ExtractorInput input, byte[] target, int offset, int length)
+ throws IOException, InterruptedException {
+ int totalBytesPeeked = 0;
+ while (totalBytesPeeked < length) {
+ int bytesPeeked = input.peek(target, offset + totalBytesPeeked, length - totalBytesPeeked);
+ if (bytesPeeked == C.RESULT_END_OF_INPUT) {
+ break;
+ }
+ totalBytesPeeked += bytesPeeked;
+ }
+ return totalBytesPeeked;
+ }
+
+ private ExtractorUtil() {}
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java
new file mode 100644
index 0000000000..64b803f65e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java
@@ -0,0 +1,23 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+/** Factory for arrays of {@link Extractor} instances. */
+public interface ExtractorsFactory {
+
+ /** Returns an array of new {@link Extractor} instances. */
+ Extractor[] createExtractors();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java
new file mode 100644
index 0000000000..e8d2b4928b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * Reads and peeks FLAC frame elements according to the <a
+ * href="https://xiph.org/flac/format.html">FLAC format specification</a>.
+ */
+public final class FlacFrameReader {
+
+ /** Holds a sample number. */
+ public static final class SampleNumberHolder {
+ /** The sample number. */
+ public long sampleNumber;
+ }
+
+ /**
+ * Checks whether the given FLAC frame header is valid and, if so, reads it and writes the frame
+ * first sample number in {@code sampleNumberHolder}.
+ *
+ * <p>If the header is valid, the position of {@code data} is moved to the byte following it.
+ * Otherwise, there is no guarantee on the position.
+ *
+ * @param data The array to read the data from, whose position must correspond to the frame
+ * header.
+ * @param flacStreamMetadata The stream metadata.
+ * @param frameStartMarker The frame start marker of the stream.
+ * @param sampleNumberHolder The holder used to contain the sample number.
+ * @return Whether the frame header is valid.
+ */
+ public static boolean checkAndReadFrameHeader(
+ ParsableByteArray data,
+ FlacStreamMetadata flacStreamMetadata,
+ int frameStartMarker,
+ SampleNumberHolder sampleNumberHolder) {
+ int frameStartPosition = data.getPosition();
+
+ long frameHeaderBytes = data.readUnsignedInt();
+ if (frameHeaderBytes >>> 16 != frameStartMarker) {
+ return false;
+ }
+
+ boolean isBlockSizeVariable = (frameHeaderBytes >>> 16 & 1) == 1;
+ int blockSizeKey = (int) (frameHeaderBytes >> 12 & 0xF);
+ int sampleRateKey = (int) (frameHeaderBytes >> 8 & 0xF);
+ int channelAssignmentKey = (int) (frameHeaderBytes >> 4 & 0xF);
+ int bitsPerSampleKey = (int) (frameHeaderBytes >> 1 & 0x7);
+ boolean reservedBit = (frameHeaderBytes & 1) == 1;
+ return checkChannelAssignment(channelAssignmentKey, flacStreamMetadata)
+ && checkBitsPerSample(bitsPerSampleKey, flacStreamMetadata)
+ && !reservedBit
+ && checkAndReadFirstSampleNumber(
+ data, flacStreamMetadata, isBlockSizeVariable, sampleNumberHolder)
+ && checkAndReadBlockSizeSamples(data, flacStreamMetadata, blockSizeKey)
+ && checkAndReadSampleRate(data, flacStreamMetadata, sampleRateKey)
+ && checkAndReadCrc(data, frameStartPosition);
+ }
+
+ /**
+ * Checks whether the given FLAC frame header is valid and, if so, writes the frame first sample
+ * number in {@code sampleNumberHolder}.
+ *
+ * <p>The {@code input} peek position is left unchanged.
+ *
+ * @param input The input to get the data from, whose peek position must correspond to the frame
+ * header.
+ * @param flacStreamMetadata The stream metadata.
+ * @param frameStartMarker The frame start marker of the stream.
+ * @param sampleNumberHolder The holder used to contain the sample number.
+ * @return Whether the frame header is valid.
+ */
+ public static boolean checkFrameHeaderFromPeek(
+ ExtractorInput input,
+ FlacStreamMetadata flacStreamMetadata,
+ int frameStartMarker,
+ SampleNumberHolder sampleNumberHolder)
+ throws IOException, InterruptedException {
+ long originalPeekPosition = input.getPeekPosition();
+
+ byte[] frameStartBytes = new byte[2];
+ input.peekFully(frameStartBytes, 0, 2);
+ int frameStart = (frameStartBytes[0] & 0xFF) << 8 | (frameStartBytes[1] & 0xFF);
+ if (frameStart != frameStartMarker) {
+ input.resetPeekPosition();
+ input.advancePeekPosition((int) (originalPeekPosition - input.getPosition()));
+ return false;
+ }
+
+ ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE);
+ System.arraycopy(
+ frameStartBytes, /* srcPos= */ 0, scratch.data, /* destPos= */ 0, /* length= */ 2);
+
+ int totalBytesPeeked =
+ ExtractorUtil.peekToLength(input, scratch.data, 2, FlacConstants.MAX_FRAME_HEADER_SIZE - 2);
+ scratch.setLimit(totalBytesPeeked);
+
+ input.resetPeekPosition();
+ input.advancePeekPosition((int) (originalPeekPosition - input.getPosition()));
+
+ return checkAndReadFrameHeader(
+ scratch, flacStreamMetadata, frameStartMarker, sampleNumberHolder);
+ }
+
+ /**
+ * Returns the number of the first sample in the given frame.
+ *
+ * <p>The read position of {@code input} is left unchanged.
+ *
+ * <p>If no exception is thrown, the peek position is aligned with the read position. Otherwise,
+ * there is no guarantee on the peek position.
+ *
+ * @param input Input stream to get the sample number from (starting from the read position).
+ * @return The frame first sample number.
+ * @throws ParserException If an error occurs parsing the sample number.
+ * @throws IOException If peeking from the input fails.
+ * @throws InterruptedException If interrupted while peeking from input.
+ */
+ public static long getFirstSampleNumber(
+ ExtractorInput input, FlacStreamMetadata flacStreamMetadata)
+ throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ input.advancePeekPosition(1);
+ byte[] blockingStrategyByte = new byte[1];
+ input.peekFully(blockingStrategyByte, 0, 1);
+ boolean isBlockSizeVariable = (blockingStrategyByte[0] & 1) == 1;
+ input.advancePeekPosition(2);
+
+ int maxUtf8SampleNumberSize = isBlockSizeVariable ? 7 : 6;
+ ParsableByteArray scratch = new ParsableByteArray(maxUtf8SampleNumberSize);
+ int totalBytesPeeked =
+ ExtractorUtil.peekToLength(input, scratch.data, 0, maxUtf8SampleNumberSize);
+ scratch.setLimit(totalBytesPeeked);
+ input.resetPeekPosition();
+
+ SampleNumberHolder sampleNumberHolder = new SampleNumberHolder();
+ if (!checkAndReadFirstSampleNumber(
+ scratch, flacStreamMetadata, isBlockSizeVariable, sampleNumberHolder)) {
+ throw new ParserException();
+ }
+
+ return sampleNumberHolder.sampleNumber;
+ }
+
+ /**
+ * Reads the given block size.
+ *
+ * @param data The array to read the data from, whose position must correspond to the block size
+ * bits.
+ * @param blockSizeKey The key in the block size lookup table.
+ * @return The block size in samples, or -1 if the {@code blockSizeKey} is invalid.
+ */
+ public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray data, int blockSizeKey) {
+ switch (blockSizeKey) {
+ case 1:
+ return 192;
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ return 576 << (blockSizeKey - 2);
+ case 6:
+ return data.readUnsignedByte() + 1;
+ case 7:
+ return data.readUnsignedShort() + 1;
+ case 8:
+ case 9:
+ case 10:
+ case 11:
+ case 12:
+ case 13:
+ case 14:
+ case 15:
+ return 256 << (blockSizeKey - 8);
+ default:
+ return -1;
+ }
+ }
+
+ /**
+ * Checks whether the given channel assignment is valid.
+ *
+ * @param channelAssignmentKey The channel assignment lookup key.
+ * @param flacStreamMetadata The stream metadata.
+ * @return Whether the channel assignment is valid.
+ */
+ private static boolean checkChannelAssignment(
+ int channelAssignmentKey, FlacStreamMetadata flacStreamMetadata) {
+ if (channelAssignmentKey <= 7) {
+ return channelAssignmentKey == flacStreamMetadata.channels - 1;
+ } else if (channelAssignmentKey <= 10) {
+ return flacStreamMetadata.channels == 2;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Checks whether the given number of bits per sample is valid.
+ *
+ * @param bitsPerSampleKey The bits per sample lookup key.
+ * @param flacStreamMetadata The stream metadata.
+ * @return Whether the number of bits per sample is valid.
+ */
+ private static boolean checkBitsPerSample(
+ int bitsPerSampleKey, FlacStreamMetadata flacStreamMetadata) {
+ if (bitsPerSampleKey == 0) {
+ return true;
+ }
+ return bitsPerSampleKey == flacStreamMetadata.bitsPerSampleLookupKey;
+ }
+
+ /**
+ * Checks whether the given sample number is valid and, if so, reads it and writes it in {@code
+ * sampleNumberHolder}.
+ *
+ * <p>If the sample number is valid, the position of {@code data} is moved to the byte following
+ * it. Otherwise, there is no guarantee on the position.
+ *
+ * @param data The array to read the data from, whose position must correspond to the sample
+ * number data.
+ * @param flacStreamMetadata The stream metadata.
+ * @param isBlockSizeVariable Whether the stream blocking strategy is variable block size or fixed
+ * block size.
+ * @param sampleNumberHolder The holder used to contain the sample number.
+ * @return Whether the sample number is valid.
+ */
+ private static boolean checkAndReadFirstSampleNumber(
+ ParsableByteArray data,
+ FlacStreamMetadata flacStreamMetadata,
+ boolean isBlockSizeVariable,
+ SampleNumberHolder sampleNumberHolder) {
+ long utf8Value;
+ try {
+ utf8Value = data.readUtf8EncodedLong();
+ } catch (NumberFormatException e) {
+ return false;
+ }
+
+ sampleNumberHolder.sampleNumber =
+ isBlockSizeVariable ? utf8Value : utf8Value * flacStreamMetadata.maxBlockSizeSamples;
+ return true;
+ }
+
+ /**
+ * Checks whether the given frame block size key and block size bits are valid and, if so, reads
+ * the block size bits.
+ *
+ * <p>If the block size is valid, the position of {@code data} is moved to the byte following the
+ * block size bits. Otherwise, there is no guarantee on the position.
+ *
+ * @param data The array to read the data from, whose position must correspond to the block size
+ * bits.
+ * @param flacStreamMetadata The stream metadata.
+ * @param blockSizeKey The key in the block size lookup table.
+ * @return Whether the block size is valid.
+ */
+ private static boolean checkAndReadBlockSizeSamples(
+ ParsableByteArray data, FlacStreamMetadata flacStreamMetadata, int blockSizeKey) {
+ int blockSizeSamples = readFrameBlockSizeSamplesFromKey(data, blockSizeKey);
+ return blockSizeSamples != -1 && blockSizeSamples <= flacStreamMetadata.maxBlockSizeSamples;
+ }
+
+ /**
+ * Checks whether the given sample rate key and sample rate bits are valid and, if so, reads the
+ * sample rate bits.
+ *
+ * <p>If the sample rate is valid, the position of {@code data} is moved to the byte following the
+ * sample rate bits. Otherwise, there is no guarantee on the position.
+ *
+ * @param data The array to read the data from, whose position must indicate the sample rate bits.
+ * @param flacStreamMetadata The stream metadata.
+ * @param sampleRateKey The key in the sample rate lookup table.
+ * @return Whether the sample rate is valid.
+ */
+ private static boolean checkAndReadSampleRate(
+ ParsableByteArray data, FlacStreamMetadata flacStreamMetadata, int sampleRateKey) {
+ int expectedSampleRate = flacStreamMetadata.sampleRate;
+ if (sampleRateKey == 0) {
+ return true;
+ } else if (sampleRateKey <= 11) {
+ return sampleRateKey == flacStreamMetadata.sampleRateLookupKey;
+ } else if (sampleRateKey == 12) {
+ return data.readUnsignedByte() * 1000 == expectedSampleRate;
+ } else if (sampleRateKey <= 14) {
+ int sampleRate = data.readUnsignedShort();
+ if (sampleRateKey == 14) {
+ sampleRate *= 10;
+ }
+ return sampleRate == expectedSampleRate;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Checks whether the given CRC is valid and, if so, reads it.
+ *
+ * <p>If the CRC is valid, the position of {@code data} is moved to the byte following it.
+ * Otherwise, there is no guarantee on the position.
+ *
+ * <p>The {@code data} array must contain the whole frame header.
+ *
+ * @param data The array to read the data from, whose position must indicate the CRC.
+ * @param frameStartPosition The frame start offset in {@code data}.
+ * @return Whether the CRC is valid.
+ */
+ private static boolean checkAndReadCrc(ParsableByteArray data, int frameStartPosition) {
+ int crc = data.readUnsignedByte();
+ int frameEndPosition = data.getPosition();
+ int expectedCrc =
+ Util.crc8(data.data, frameStartPosition, frameEndPosition - 1, /* initialValue= */ 0);
+ return crc == expectedCrc;
+ }
+
+ private FlacFrameReader() {}
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java
new file mode 100644
index 0000000000..c5413cf459
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil.CommentHeader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac.PictureFrame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Reads and peeks FLAC stream metadata elements according to the <a
+ * href="https://xiph.org/flac/format.html">FLAC format specification</a>.
+ */
+public final class FlacMetadataReader {
+
+ /** Holds a {@link FlacStreamMetadata}. */
+ public static final class FlacStreamMetadataHolder {
+ /** The FLAC stream metadata. */
+ @Nullable public FlacStreamMetadata flacStreamMetadata;
+
+ public FlacStreamMetadataHolder(@Nullable FlacStreamMetadata flacStreamMetadata) {
+ this.flacStreamMetadata = flacStreamMetadata;
+ }
+ }
+
+ private static final int STREAM_MARKER = 0x664C6143; // ASCII for "fLaC"
+ private static final int SYNC_CODE = 0x3FFE;
+ private static final int SEEK_POINT_SIZE = 18;
+
+ /**
+ * Peeks ID3 Data.
+ *
+ * @param input Input stream to peek the ID3 data from.
+ * @param parseData Whether to parse the ID3 frames.
+ * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData}
+ * is {@code false}.
+ * @throws IOException If peeking from the input fails. In this case, there is no guarantee on the
+ * peek position.
+ * @throws InterruptedException If interrupted while peeking from input. In this case, there is no
+ * guarantee on the peek position.
+ */
+ @Nullable
+ public static Metadata peekId3Metadata(ExtractorInput input, boolean parseData)
+ throws IOException, InterruptedException {
+ @Nullable
+ Id3Decoder.FramePredicate id3FramePredicate = parseData ? null : Id3Decoder.NO_FRAMES_PREDICATE;
+ @Nullable Metadata id3Metadata = new Id3Peeker().peekId3Data(input, id3FramePredicate);
+ return id3Metadata == null || id3Metadata.length() == 0 ? null : id3Metadata;
+ }
+
+ /**
+ * Peeks the FLAC stream marker.
+ *
+ * @param input Input stream to peek the stream marker from.
+ * @return Whether the data peeked is the FLAC stream marker.
+ * @throws IOException If peeking from the input fails. In this case, the peek position is left
+ * unchanged.
+ * @throws InterruptedException If interrupted while peeking from input. In this case, the peek
+ * position is left unchanged.
+ */
+ public static boolean checkAndPeekStreamMarker(ExtractorInput input)
+ throws IOException, InterruptedException {
+ ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE);
+ input.peekFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE);
+ return scratch.readUnsignedInt() == STREAM_MARKER;
+ }
+
+ /**
+ * Reads ID3 Data.
+ *
+ * <p>If no exception is thrown, the peek position of {@code input} is aligned with the read
+ * position.
+ *
+ * @param input Input stream to read the ID3 data from.
+ * @param parseData Whether to parse the ID3 frames.
+ * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData}
+ * is {@code false}.
+ * @throws IOException If reading from the input fails. In this case, the read position is left
+ * unchanged and there is no guarantee on the peek position.
+ * @throws InterruptedException If interrupted while reading from input. In this case, the read
+ * position is left unchanged and there is no guarantee on the peek position.
+ */
+ @Nullable
+ public static Metadata readId3Metadata(ExtractorInput input, boolean parseData)
+ throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ long startingPeekPosition = input.getPeekPosition();
+ @Nullable Metadata id3Metadata = peekId3Metadata(input, parseData);
+ int peekedId3Bytes = (int) (input.getPeekPosition() - startingPeekPosition);
+ input.skipFully(peekedId3Bytes);
+ return id3Metadata;
+ }
+
+ /**
+ * Reads the FLAC stream marker.
+ *
+ * @param input Input stream to read the stream marker from.
+ * @throws ParserException If an error occurs parsing the stream marker. In this case, the
+ * position of {@code input} is advanced by {@link FlacConstants#STREAM_MARKER_SIZE} bytes.
+ * @throws IOException If reading from the input fails. In this case, the position is left
+ * unchanged.
+ * @throws InterruptedException If interrupted while reading from input. In this case, the
+ * position is left unchanged.
+ */
+ public static void readStreamMarker(ExtractorInput input)
+ throws IOException, InterruptedException {
+ ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE);
+ input.readFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE);
+ if (scratch.readUnsignedInt() != STREAM_MARKER) {
+ throw new ParserException("Failed to read FLAC stream marker.");
+ }
+ }
+
+ /**
+ * Reads one FLAC metadata block.
+ *
+ * <p>If no exception is thrown, the peek position of {@code input} is aligned with the read
+ * position.
+ *
+ * @param input Input stream to read the metadata block from (header included).
+ * @param metadataHolder A holder for the metadata read. If the stream info block (which must be
+ * the first metadata block) is read, the holder contains a new instance representing the
+ * stream info data. If the block read is a Vorbis comment block or a picture block, the
+ * holder contains a copy of the existing stream metadata with the corresponding metadata
+ * added. Otherwise, the metadata in the holder is unchanged.
+ * @return Whether the block read is the last metadata block.
+ * @throws IllegalArgumentException If the block read is not a stream info block and the metadata
+ * in {@code metadataHolder} is {@code null}. In this case, the read position will be at the
+ * start of a metadata block and there is no guarantee on the peek position.
+ * @throws IOException If reading from the input fails. In this case, the read position will be at
+ * the start of a metadata block and there is no guarantee on the peek position.
+ * @throws InterruptedException If interrupted while reading from input. In this case, the read
+ * position will be at the start of a metadata block and there is no guarantee on the peek
+ * position.
+ */
+ public static boolean readMetadataBlock(
+ ExtractorInput input, FlacStreamMetadataHolder metadataHolder)
+ throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ ParsableBitArray scratch = new ParsableBitArray(new byte[4]);
+ input.peekFully(scratch.data, 0, FlacConstants.METADATA_BLOCK_HEADER_SIZE);
+
+ boolean isLastMetadataBlock = scratch.readBit();
+ int type = scratch.readBits(7);
+ int length = FlacConstants.METADATA_BLOCK_HEADER_SIZE + scratch.readBits(24);
+ if (type == FlacConstants.METADATA_TYPE_STREAM_INFO) {
+ metadataHolder.flacStreamMetadata = readStreamInfoBlock(input);
+ } else {
+ FlacStreamMetadata flacStreamMetadata = metadataHolder.flacStreamMetadata;
+ if (flacStreamMetadata == null) {
+ throw new IllegalArgumentException();
+ }
+ if (type == FlacConstants.METADATA_TYPE_SEEK_TABLE) {
+ FlacStreamMetadata.SeekTable seekTable = readSeekTableMetadataBlock(input, length);
+ metadataHolder.flacStreamMetadata = flacStreamMetadata.copyWithSeekTable(seekTable);
+ } else if (type == FlacConstants.METADATA_TYPE_VORBIS_COMMENT) {
+ List<String> vorbisComments = readVorbisCommentMetadataBlock(input, length);
+ metadataHolder.flacStreamMetadata =
+ flacStreamMetadata.copyWithVorbisComments(vorbisComments);
+ } else if (type == FlacConstants.METADATA_TYPE_PICTURE) {
+ PictureFrame pictureFrame = readPictureMetadataBlock(input, length);
+ metadataHolder.flacStreamMetadata =
+ flacStreamMetadata.copyWithPictureFrames(Collections.singletonList(pictureFrame));
+ } else {
+ input.skipFully(length);
+ }
+ }
+
+ return isLastMetadataBlock;
+ }
+
+ /**
+ * Reads a FLAC seek table metadata block.
+ *
+ * <p>The position of {@code data} is moved to the byte following the seek table metadata block
+ * (placeholder points included).
+ *
+ * @param data The array to read the data from, whose position must correspond to the seek table
+ * metadata block (header included).
+ * @return The seek table, without the placeholder points.
+ */
+ public static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock(ParsableByteArray data) {
+ data.skipBytes(1);
+ int length = data.readUnsignedInt24();
+
+ long seekTableEndPosition = data.getPosition() + length;
+ int seekPointCount = length / SEEK_POINT_SIZE;
+ long[] pointSampleNumbers = new long[seekPointCount];
+ long[] pointOffsets = new long[seekPointCount];
+ for (int i = 0; i < seekPointCount; i++) {
+ // The sample number is expected to fit in a signed long, except if it is a placeholder, in
+ // which case its value is -1.
+ long sampleNumber = data.readLong();
+ if (sampleNumber == -1) {
+ pointSampleNumbers = Arrays.copyOf(pointSampleNumbers, i);
+ pointOffsets = Arrays.copyOf(pointOffsets, i);
+ break;
+ }
+ pointSampleNumbers[i] = sampleNumber;
+ pointOffsets[i] = data.readLong();
+ data.skipBytes(2);
+ }
+
+ data.skipBytes((int) (seekTableEndPosition - data.getPosition()));
+ return new FlacStreamMetadata.SeekTable(pointSampleNumbers, pointOffsets);
+ }
+
+ /**
+ * Returns the frame start marker, consisting of the 2 first bytes of the first frame.
+ *
+ * <p>The read position of {@code input} is left unchanged and the peek position is aligned with
+ * the read position.
+ *
+ * @param input Input stream to get the start marker from (starting from the read position).
+ * @return The frame start marker (which must be the same for all the frames in the stream).
+ * @throws ParserException If an error occurs parsing the frame start marker.
+ * @throws IOException If peeking from the input fails.
+ * @throws InterruptedException If interrupted while peeking from input.
+ */
+ public static int getFrameStartMarker(ExtractorInput input)
+ throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ ParsableByteArray scratch = new ParsableByteArray(2);
+ input.peekFully(scratch.data, 0, 2);
+
+ int frameStartMarker = scratch.readUnsignedShort();
+ int syncCode = frameStartMarker >> 2;
+ if (syncCode != SYNC_CODE) {
+ input.resetPeekPosition();
+ throw new ParserException("First frame does not start with sync code.");
+ }
+
+ input.resetPeekPosition();
+ return frameStartMarker;
+ }
+
+ private static FlacStreamMetadata readStreamInfoBlock(ExtractorInput input)
+ throws IOException, InterruptedException {
+ byte[] scratchData = new byte[FlacConstants.STREAM_INFO_BLOCK_SIZE];
+ input.readFully(scratchData, 0, FlacConstants.STREAM_INFO_BLOCK_SIZE);
+ return new FlacStreamMetadata(
+ scratchData, /* offset= */ FlacConstants.METADATA_BLOCK_HEADER_SIZE);
+ }
+
+ private static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock(
+ ExtractorInput input, int length) throws IOException, InterruptedException {
+ ParsableByteArray scratch = new ParsableByteArray(length);
+ input.readFully(scratch.data, 0, length);
+ return readSeekTableMetadataBlock(scratch);
+ }
+
+ private static List<String> readVorbisCommentMetadataBlock(ExtractorInput input, int length)
+ throws IOException, InterruptedException {
+ ParsableByteArray scratch = new ParsableByteArray(length);
+ input.readFully(scratch.data, 0, length);
+ scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);
+ CommentHeader commentHeader =
+ VorbisUtil.readVorbisCommentHeader(
+ scratch, /* hasMetadataHeader= */ false, /* hasFramingBit= */ false);
+ return Arrays.asList(commentHeader.comments);
+ }
+
+ private static PictureFrame readPictureMetadataBlock(ExtractorInput input, int length)
+ throws IOException, InterruptedException {
+ ParsableByteArray scratch = new ParsableByteArray(length);
+ input.readFully(scratch.data, 0, length);
+ scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);
+
+ int pictureType = scratch.readInt();
+ int mimeTypeLength = scratch.readInt();
+ String mimeType = scratch.readString(mimeTypeLength, Charset.forName(C.ASCII_NAME));
+ int descriptionLength = scratch.readInt();
+ String description = scratch.readString(descriptionLength);
+ int width = scratch.readInt();
+ int height = scratch.readInt();
+ int depth = scratch.readInt();
+ int colors = scratch.readInt();
+ int pictureDataLength = scratch.readInt();
+ byte[] pictureData = new byte[pictureDataLength];
+ scratch.readBytes(pictureData, 0, pictureDataLength);
+
+ return new PictureFrame(
+ pictureType, mimeType, description, width, height, depth, colors, pictureData);
+ }
+
+ private FlacMetadataReader() {}
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java
new file mode 100644
index 0000000000..56d54596ac
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/**
+ * A {@link SeekMap} implementation for FLAC streams that contain a <a
+ * href="https://xiph.org/flac/format.html#metadata_block_seektable">seek table</a>.
+ */
+public final class FlacSeekTableSeekMap implements SeekMap {
+
+ private final FlacStreamMetadata flacStreamMetadata;
+ private final long firstFrameOffset;
+
+ /**
+ * Creates a seek map from the FLAC stream seek table.
+ *
+ * @param flacStreamMetadata The stream metadata.
+ * @param firstFrameOffset The byte offset of the first frame in the stream.
+ */
+ public FlacSeekTableSeekMap(FlacStreamMetadata flacStreamMetadata, long firstFrameOffset) {
+ this.flacStreamMetadata = flacStreamMetadata;
+ this.firstFrameOffset = firstFrameOffset;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return flacStreamMetadata.getDurationUs();
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ Assertions.checkNotNull(flacStreamMetadata.seekTable);
+ long[] pointSampleNumbers = flacStreamMetadata.seekTable.pointSampleNumbers;
+ long[] pointOffsets = flacStreamMetadata.seekTable.pointOffsets;
+
+ long targetSampleNumber = flacStreamMetadata.getSampleNumber(timeUs);
+ int index =
+ Util.binarySearchFloor(
+ pointSampleNumbers,
+ targetSampleNumber,
+ /* inclusive= */ true,
+ /* stayInBounds= */ false);
+
+ long seekPointSampleNumber = index == -1 ? 0 : pointSampleNumbers[index];
+ long seekPointOffsetFromFirstFrame = index == -1 ? 0 : pointOffsets[index];
+ SeekPoint seekPoint = getSeekPoint(seekPointSampleNumber, seekPointOffsetFromFirstFrame);
+ if (seekPoint.timeUs == timeUs || index == pointSampleNumbers.length - 1) {
+ return new SeekPoints(seekPoint);
+ } else {
+ SeekPoint secondSeekPoint =
+ getSeekPoint(pointSampleNumbers[index + 1], pointOffsets[index + 1]);
+ return new SeekPoints(seekPoint, secondSeekPoint);
+ }
+ }
+
+ private SeekPoint getSeekPoint(long sampleNumber, long offsetFromFirstFrame) {
+ long seekTimeUs = sampleNumber * C.MICROS_PER_SECOND / flacStreamMetadata.sampleRate;
+ long seekPosition = firstFrameOffset + offsetFromFirstFrame;
+ return new SeekPoint(seekTimeUs, seekPosition);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java
new file mode 100644
index 0000000000..11893d6136
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java
@@ -0,0 +1,131 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.CommentFrame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.InternalFrame;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Holder for gapless playback information.
+ */
+public final class GaplessInfoHolder {
+
+ private static final String GAPLESS_DOMAIN = "com.apple.iTunes";
+ private static final String GAPLESS_DESCRIPTION = "iTunSMPB";
+ private static final Pattern GAPLESS_COMMENT_PATTERN =
+ Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
+
+ /**
+ * The number of samples to trim from the start of the decoded audio stream, or
+ * {@link Format#NO_VALUE} if not set.
+ */
+ public int encoderDelay;
+
+ /**
+ * The number of samples to trim from the end of the decoded audio stream, or
+ * {@link Format#NO_VALUE} if not set.
+ */
+ public int encoderPadding;
+
+ /**
+ * Creates a new holder for gapless playback information.
+ */
+ public GaplessInfoHolder() {
+ encoderDelay = Format.NO_VALUE;
+ encoderPadding = Format.NO_VALUE;
+ }
+
+ /**
+ * Populates the holder with data from an MP3 Xing header, if valid and non-zero.
+ *
+ * @param value The 24-bit value to decode.
+ * @return Whether the holder was populated.
+ */
+ public boolean setFromXingHeaderValue(int value) {
+ int encoderDelay = value >> 12;
+ int encoderPadding = value & 0x0FFF;
+ if (encoderDelay > 0 || encoderPadding > 0) {
+ this.encoderDelay = encoderDelay;
+ this.encoderPadding = encoderPadding;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Populates the holder with data parsed from ID3 {@link Metadata}.
+ *
+ * @param metadata The metadata from which to parse the gapless information.
+ * @return Whether the holder was populated.
+ */
+ public boolean setFromMetadata(Metadata metadata) {
+ for (int i = 0; i < metadata.length(); i++) {
+ Metadata.Entry entry = metadata.get(i);
+ if (entry instanceof CommentFrame) {
+ CommentFrame commentFrame = (CommentFrame) entry;
+ if (GAPLESS_DESCRIPTION.equals(commentFrame.description)
+ && setFromComment(commentFrame.text)) {
+ return true;
+ }
+ } else if (entry instanceof InternalFrame) {
+ InternalFrame internalFrame = (InternalFrame) entry;
+ if (GAPLESS_DOMAIN.equals(internalFrame.domain)
+ && GAPLESS_DESCRIPTION.equals(internalFrame.description)
+ && setFromComment(internalFrame.text)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header
+ * or MPEG 4 user data), if valid and non-zero.
+ *
+ * @param data The comment's payload data.
+ * @return Whether the holder was populated.
+ */
+ private boolean setFromComment(String data) {
+ Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
+ if (matcher.find()) {
+ try {
+ int encoderDelay = Integer.parseInt(matcher.group(1), 16);
+ int encoderPadding = Integer.parseInt(matcher.group(2), 16);
+ if (encoderDelay > 0 || encoderPadding > 0) {
+ this.encoderDelay = encoderDelay;
+ this.encoderPadding = encoderPadding;
+ return true;
+ }
+ } catch (NumberFormatException e) {
+ // Ignore incorrectly formatted comments.
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set.
+ */
+ public boolean hasGaplessInfo() {
+ return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java
new file mode 100644
index 0000000000..a0a26c76d8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Peeks data from the beginning of an {@link ExtractorInput} to determine if there is any ID3 tag.
+ */
+public final class Id3Peeker {
+
+ private final ParsableByteArray scratch;
+
+ public Id3Peeker() {
+ scratch = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
+ }
+
+ /**
+ * Peeks ID3 data from the input and parses the first ID3 tag.
+ *
+ * @param input The {@link ExtractorInput} from which data should be peeked.
+ * @param id3FramePredicate Determines which ID3 frames are decoded. May be null to decode all
+ * frames.
+ * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not
+ * present in the input.
+ * @throws IOException If an error occurred peeking from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ @Nullable
+ public Metadata peekId3Data(
+ ExtractorInput input, @Nullable Id3Decoder.FramePredicate id3FramePredicate)
+ throws IOException, InterruptedException {
+ int peekedId3Bytes = 0;
+ Metadata metadata = null;
+ while (true) {
+ try {
+ input.peekFully(scratch.data, /* offset= */ 0, Id3Decoder.ID3_HEADER_LENGTH);
+ } catch (EOFException e) {
+ // If input has less than ID3_HEADER_LENGTH, ignore the rest.
+ break;
+ }
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) {
+ // Not an ID3 tag.
+ break;
+ }
+ scratch.skipBytes(3); // Skip major version, minor version and flags.
+ int framesLength = scratch.readSynchSafeInt();
+ int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength;
+
+ if (metadata == null) {
+ byte[] id3Data = new byte[tagLength];
+ System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);
+ input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength);
+
+ metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength);
+ } else {
+ input.advancePeekPosition(framesLength);
+ }
+
+ peekedId3Bytes += tagLength;
+ }
+
+ input.resetPeekPosition();
+ input.advancePeekPosition(peekedId3Bytes);
+ return metadata;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java
new file mode 100644
index 0000000000..66c3411094
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java
@@ -0,0 +1,275 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+
+/**
+ * An MPEG audio frame header.
+ */
+public final class MpegAudioHeader {
+
+ /**
+ * Theoretical maximum frame size for an MPEG audio stream, which occurs when playing a Layer 2
+ * MPEG 2.5 audio stream at 16 kb/s (with padding). The size is 1152 sample/frame *
+ * 160000 bit/s / (8000 sample/s * 8 bit/byte) + 1 padding byte/frame = 2881 byte/frame.
+ * The next power of two size is 4 KiB.
+ */
+ public static final int MAX_FRAME_SIZE_BYTES = 4096;
+
+ private static final String[] MIME_TYPE_BY_LAYER =
+ new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG};
+ private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000};
+ private static final int[] BITRATE_V1_L1 = {
+ 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000,
+ 416000, 448000
+ };
+ private static final int[] BITRATE_V2_L1 = {
+ 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000,
+ 224000, 256000
+ };
+ private static final int[] BITRATE_V1_L2 = {
+ 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000,
+ 320000, 384000
+ };
+ private static final int[] BITRATE_V1_L3 = {
+ 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000,
+ 320000
+ };
+ private static final int[] BITRATE_V2 = {
+ 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000,
+ 160000
+ };
+
+ private static final int SAMPLES_PER_FRAME_L1 = 384;
+ private static final int SAMPLES_PER_FRAME_L2 = 1152;
+ private static final int SAMPLES_PER_FRAME_L3_V1 = 1152;
+ private static final int SAMPLES_PER_FRAME_L3_V2 = 576;
+
+ /**
+ * Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it
+ * is invalid.
+ */
+ public static int getFrameSize(int header) {
+ if (!isMagicPresent(header)) {
+ return C.LENGTH_UNSET;
+ }
+
+ int version = (header >>> 19) & 3;
+ if (version == 1) {
+ return C.LENGTH_UNSET;
+ }
+
+ int layer = (header >>> 17) & 3;
+ if (layer == 0) {
+ return C.LENGTH_UNSET;
+ }
+
+ int bitrateIndex = (header >>> 12) & 15;
+ if (bitrateIndex == 0 || bitrateIndex == 0xF) {
+ // Disallow "free" bitrate.
+ return C.LENGTH_UNSET;
+ }
+
+ int samplingRateIndex = (header >>> 10) & 3;
+ if (samplingRateIndex == 3) {
+ return C.LENGTH_UNSET;
+ }
+
+ int samplingRate = SAMPLING_RATE_V1[samplingRateIndex];
+ if (version == 2) {
+ // Version 2
+ samplingRate /= 2;
+ } else if (version == 0) {
+ // Version 2.5
+ samplingRate /= 4;
+ }
+
+ int bitrate;
+ int padding = (header >>> 9) & 1;
+ if (layer == 3) {
+ // Layer I (layer == 3)
+ bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
+ return (12 * bitrate / samplingRate + padding) * 4;
+ } else {
+ // Layer II (layer == 2) or III (layer == 1)
+ if (version == 3) {
+ bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
+ } else {
+ // Version 2 or 2.5.
+ bitrate = BITRATE_V2[bitrateIndex - 1];
+ }
+ }
+
+ if (version == 3) {
+ // Version 1
+ return 144 * bitrate / samplingRate + padding;
+ } else {
+ // Version 2 or 2.5
+ return (layer == 1 ? 72 : 144) * bitrate / samplingRate + padding;
+ }
+ }
+
+ /**
+ * Returns the number of samples per frame associated with {@code header}, or {@link
+ * C#LENGTH_UNSET} if it is invalid.
+ */
+ public static int getFrameSampleCount(int header) {
+
+ if (!isMagicPresent(header)) {
+ return C.LENGTH_UNSET;
+ }
+
+ int version = (header >>> 19) & 3;
+ if (version == 1) {
+ return C.LENGTH_UNSET;
+ }
+
+ int layer = (header >>> 17) & 3;
+ if (layer == 0) {
+ return C.LENGTH_UNSET;
+ }
+
+ // Those header values are not used but are checked for consistency with the other methods
+ int bitrateIndex = (header >>> 12) & 15;
+ int samplingRateIndex = (header >>> 10) & 3;
+ if (bitrateIndex == 0 || bitrateIndex == 0xF || samplingRateIndex == 3) {
+ return C.LENGTH_UNSET;
+ }
+
+ return getFrameSizeInSamples(version, layer);
+ }
+
+ /**
+ * Parses {@code headerData}, populating {@code header} with the parsed data.
+ *
+ * @param headerData Header data to parse.
+ * @param header Header to populate with data from {@code headerData}.
+ * @return True if the header was populated. False otherwise, indicating that {@code headerData}
+ * is not a valid MPEG audio header.
+ */
+ public static boolean populateHeader(int headerData, MpegAudioHeader header) {
+ if (!isMagicPresent(headerData)) {
+ return false;
+ }
+
+ int version = (headerData >>> 19) & 3;
+ if (version == 1) {
+ return false;
+ }
+
+ int layer = (headerData >>> 17) & 3;
+ if (layer == 0) {
+ return false;
+ }
+
+ int bitrateIndex = (headerData >>> 12) & 15;
+ if (bitrateIndex == 0 || bitrateIndex == 0xF) {
+ // Disallow "free" bitrate.
+ return false;
+ }
+
+ int samplingRateIndex = (headerData >>> 10) & 3;
+ if (samplingRateIndex == 3) {
+ return false;
+ }
+
+ int sampleRate = SAMPLING_RATE_V1[samplingRateIndex];
+ if (version == 2) {
+ // Version 2
+ sampleRate /= 2;
+ } else if (version == 0) {
+ // Version 2.5
+ sampleRate /= 4;
+ }
+
+ int padding = (headerData >>> 9) & 1;
+ int bitrate;
+ int frameSize;
+ int samplesPerFrame = getFrameSizeInSamples(version, layer);
+ if (layer == 3) {
+ // Layer I (layer == 3)
+ bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
+ frameSize = (12 * bitrate / sampleRate + padding) * 4;
+ } else {
+ // Layer II (layer == 2) or III (layer == 1)
+ if (version == 3) {
+ // Version 1
+ bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
+ frameSize = 144 * bitrate / sampleRate + padding;
+ } else {
+ // Version 2 or 2.5.
+ bitrate = BITRATE_V2[bitrateIndex - 1];
+ frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding;
+ }
+ }
+
+ String mimeType = MIME_TYPE_BY_LAYER[3 - layer];
+ int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
+ header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame);
+ return true;
+ }
+
+ private static boolean isMagicPresent(int header) {
+ return (header & 0xFFE00000) == 0xFFE00000;
+ }
+
+ private static int getFrameSizeInSamples(int version, int layer) {
+ switch (layer) {
+ case 1:
+ return version == 3 ? SAMPLES_PER_FRAME_L3_V1 : SAMPLES_PER_FRAME_L3_V2; // Layer III
+ case 2:
+ return SAMPLES_PER_FRAME_L2; // Layer II
+ case 3:
+ return SAMPLES_PER_FRAME_L1; // Layer I
+ }
+ throw new IllegalArgumentException();
+ }
+
+ /** MPEG audio header version. */
+ public int version;
+ /** The mime type. */
+ @Nullable public String mimeType;
+ /** Size of the frame associated with this header, in bytes. */
+ public int frameSize;
+ /** Sample rate in samples per second. */
+ public int sampleRate;
+ /** Number of audio channels in the frame. */
+ public int channels;
+ /** Bitrate of the frame in bit/s. */
+ public int bitrate;
+ /** Number of samples stored in the frame. */
+ public int samplesPerFrame;
+
+ private void setValues(
+ int version,
+ String mimeType,
+ int frameSize,
+ int sampleRate,
+ int channels,
+ int bitrate,
+ int samplesPerFrame) {
+ this.version = version;
+ this.mimeType = mimeType;
+ this.frameSize = frameSize;
+ this.sampleRate = sampleRate;
+ this.channels = channels;
+ this.bitrate = bitrate;
+ this.samplesPerFrame = samplesPerFrame;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/PositionHolder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/PositionHolder.java
new file mode 100644
index 0000000000..feae7f0bc7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/PositionHolder.java
@@ -0,0 +1,28 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+/**
+ * Holds a position in the stream.
+ */
+public final class PositionHolder {
+
+ /**
+ * The held position.
+ */
+ public long position;
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java
new file mode 100644
index 0000000000..b3ccad214d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java
@@ -0,0 +1,141 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream.
+ */
+public interface SeekMap {
+
+ /** A {@link SeekMap} that does not support seeking. */
+ class Unseekable implements SeekMap {
+
+ private final long durationUs;
+ private final SeekPoints startSeekPoints;
+
+ /**
+ * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the
+ * duration is unknown.
+ */
+ public Unseekable(long durationUs) {
+ this(durationUs, 0);
+ }
+
+ /**
+ * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the
+ * duration is unknown.
+ * @param startPosition The position (byte offset) of the start of the media.
+ */
+ public Unseekable(long durationUs, long startPosition) {
+ this.durationUs = durationUs;
+ startSeekPoints =
+ new SeekPoints(startPosition == 0 ? SeekPoint.START : new SeekPoint(0, startPosition));
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return false;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ return startSeekPoints;
+ }
+ }
+
+ /** Contains one or two {@link SeekPoint}s. */
+ final class SeekPoints {
+
+ /** The first seek point. */
+ public final SeekPoint first;
+ /** The second seek point, or {@link #first} if there's only one seek point. */
+ public final SeekPoint second;
+
+ /** @param point The single seek point. */
+ public SeekPoints(SeekPoint point) {
+ this(point, point);
+ }
+
+ /**
+ * @param first The first seek point.
+ * @param second The second seek point.
+ */
+ public SeekPoints(SeekPoint first, SeekPoint second) {
+ this.first = Assertions.checkNotNull(first);
+ this.second = Assertions.checkNotNull(second);
+ }
+
+ @Override
+ public String toString() {
+ return "[" + first + (first.equals(second) ? "" : (", " + second)) + "]";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ SeekPoints other = (SeekPoints) obj;
+ return first.equals(other.first) && second.equals(other.second);
+ }
+
+ @Override
+ public int hashCode() {
+ return (31 * first.hashCode()) + second.hashCode();
+ }
+ }
+
+ /**
+ * Returns whether seeking is supported.
+ *
+ * @return Whether seeking is supported.
+ */
+ boolean isSeekable();
+
+ /**
+ * Returns the duration of the stream in microseconds.
+ *
+ * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the duration is
+ * unknown.
+ */
+ long getDurationUs();
+
+ /**
+ * Obtains seek points for the specified seek time in microseconds. The returned {@link
+ * SeekPoints} will contain one or two distinct seek points.
+ *
+ * <p>Two seek points [A, B] are returned in the case that seeking can only be performed to
+ * discrete points in time, there does not exist a seek point at exactly the requested time, and
+ * there exist seek points on both sides of it. In this case A and B are the closest seek points
+ * before and after the requested time. A single seek point is returned in all other cases.
+ *
+ * @param timeUs A seek time in microseconds.
+ * @return The corresponding seek points.
+ */
+ SeekPoints getSeekPoints(long timeUs);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java
new file mode 100644
index 0000000000..1c4db35203
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import androidx.annotation.Nullable;
+
+/** Defines a seek point in a media stream. */
+public final class SeekPoint {
+
+ /** A {@link SeekPoint} whose time and byte offset are both set to 0. */
+ public static final SeekPoint START = new SeekPoint(0, 0);
+
+ /** The time of the seek point, in microseconds. */
+ public final long timeUs;
+
+ /** The byte offset of the seek point. */
+ public final long position;
+
+ /**
+ * @param timeUs The time of the seek point, in microseconds.
+ * @param position The byte offset of the seek point.
+ */
+ public SeekPoint(long timeUs, long position) {
+ this.timeUs = timeUs;
+ this.position = position;
+ }
+
+ @Override
+ public String toString() {
+ return "[timeUs=" + timeUs + ", position=" + position + "]";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ SeekPoint other = (SeekPoint) obj;
+ return timeUs == other.timeUs && position == other.position;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) timeUs;
+ result = 31 * result + (int) position;
+ return result;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java
new file mode 100644
index 0000000000..fd33bd6027
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java
@@ -0,0 +1,147 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Receives track level data extracted by an {@link Extractor}.
+ */
+public interface TrackOutput {
+
+ /**
+ * Holds data required to decrypt a sample.
+ */
+ final class CryptoData {
+
+ /**
+ * The encryption mode used for the sample.
+ */
+ @C.CryptoMode public final int cryptoMode;
+
+ /**
+ * The encryption key associated with the sample. Its contents must not be modified.
+ */
+ public final byte[] encryptionKey;
+
+ /**
+ * The number of encrypted blocks in the encryption pattern, 0 if pattern encryption does not
+ * apply.
+ */
+ public final int encryptedBlocks;
+
+ /**
+ * The number of clear blocks in the encryption pattern, 0 if pattern encryption does not
+ * apply.
+ */
+ public final int clearBlocks;
+
+ /**
+ * @param cryptoMode See {@link #cryptoMode}.
+ * @param encryptionKey See {@link #encryptionKey}.
+ * @param encryptedBlocks See {@link #encryptedBlocks}.
+ * @param clearBlocks See {@link #clearBlocks}.
+ */
+ public CryptoData(@C.CryptoMode int cryptoMode, byte[] encryptionKey, int encryptedBlocks,
+ int clearBlocks) {
+ this.cryptoMode = cryptoMode;
+ this.encryptionKey = encryptionKey;
+ this.encryptedBlocks = encryptedBlocks;
+ this.clearBlocks = clearBlocks;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ CryptoData other = (CryptoData) obj;
+ return cryptoMode == other.cryptoMode && encryptedBlocks == other.encryptedBlocks
+ && clearBlocks == other.clearBlocks && Arrays.equals(encryptionKey, other.encryptionKey);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = cryptoMode;
+ result = 31 * result + Arrays.hashCode(encryptionKey);
+ result = 31 * result + encryptedBlocks;
+ result = 31 * result + clearBlocks;
+ return result;
+ }
+
+ }
+
+ /**
+ * Called when the {@link Format} of the track has been extracted from the stream.
+ *
+ * @param format The extracted {@link Format}.
+ */
+ void format(Format format);
+
+ /**
+ * Called to write sample data to the output.
+ *
+ * @param input An {@link ExtractorInput} from which to read the sample data.
+ * @param length The maximum length to read from the input.
+ * @param allowEndOfInput True if encountering the end of the input having read no data is
+ * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it
+ * should be considered an error, causing an {@link EOFException} to be thrown.
+ * @return The number of bytes appended.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException;
+
+ /**
+ * Called to write sample data to the output.
+ *
+ * @param data A {@link ParsableByteArray} from which to read the sample data.
+ * @param length The number of bytes to read, starting from {@code data.getPosition()}.
+ */
+ void sampleData(ParsableByteArray data, int length);
+
+ /**
+ * Called when metadata associated with a sample has been extracted from the stream.
+ *
+ * <p>The corresponding sample data will have already been passed to the output via calls to
+ * {@link #sampleData(ExtractorInput, int, boolean)} or {@link #sampleData(ParsableByteArray,
+ * int)}.
+ *
+ * @param timeUs The media timestamp associated with the sample, in microseconds.
+ * @param flags Flags associated with the sample. See {@code C.BUFFER_FLAG_*}.
+ * @param size The size of the sample data, in bytes.
+ * @param offset The number of bytes that have been passed to {@link #sampleData(ExtractorInput,
+ * int, boolean)} or {@link #sampleData(ParsableByteArray, int)} since the last byte belonging
+ * to the sample whose metadata is being passed.
+ * @param encryptionData The encryption data required to decrypt the sample. May be null.
+ */
+ void sampleMetadata(
+ long timeUs,
+ @C.BufferFlags int flags,
+ int size,
+ int offset,
+ @Nullable CryptoData encryptionData);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java
new file mode 100644
index 0000000000..4ea27c0149
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java
@@ -0,0 +1,129 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * Wraps a byte array, providing methods that allow it to be read as a Vorbis bitstream.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-360002">Vorbis bitpacking
+ * specification</a>
+ */
+public final class VorbisBitArray {
+
+ private final byte[] data;
+ private final int byteLimit;
+
+ private int byteOffset;
+ private int bitOffset;
+
+ /**
+ * Creates a new instance that wraps an existing array.
+ *
+ * @param data the array to wrap.
+ */
+ public VorbisBitArray(byte[] data) {
+ this.data = data;
+ byteLimit = data.length;
+ }
+
+ /**
+ * Resets the reading position to zero.
+ */
+ public void reset() {
+ byteOffset = 0;
+ bitOffset = 0;
+ }
+
+ /**
+ * Reads a single bit.
+ *
+ * @return {@code true} if the bit is set, {@code false} otherwise.
+ */
+ public boolean readBit() {
+ boolean returnValue = (((data[byteOffset] & 0xFF) >> bitOffset) & 0x01) == 1;
+ skipBits(1);
+ return returnValue;
+ }
+
+ /**
+ * Reads up to 32 bits.
+ *
+ * @param numBits The number of bits to read.
+ * @return An integer whose bottom {@code numBits} bits hold the read data.
+ */
+ public int readBits(int numBits) {
+ int tempByteOffset = byteOffset;
+ int bitsRead = Math.min(numBits, 8 - bitOffset);
+ int returnValue = ((data[tempByteOffset++] & 0xFF) >> bitOffset) & (0xFF >> (8 - bitsRead));
+ while (bitsRead < numBits) {
+ returnValue |= (data[tempByteOffset++] & 0xFF) << bitsRead;
+ bitsRead += 8;
+ }
+ returnValue &= 0xFFFFFFFF >>> (32 - numBits);
+ skipBits(numBits);
+ return returnValue;
+ }
+
+ /**
+ * Skips {@code numberOfBits} bits.
+ *
+ * @param numBits The number of bits to skip.
+ */
+ public void skipBits(int numBits) {
+ int numBytes = numBits / 8;
+ byteOffset += numBytes;
+ bitOffset += numBits - (numBytes * 8);
+ if (bitOffset > 7) {
+ byteOffset++;
+ bitOffset -= 8;
+ }
+ assertValidOffset();
+ }
+
+ /**
+ * Returns the reading position in bits.
+ */
+ public int getPosition() {
+ return byteOffset * 8 + bitOffset;
+ }
+
+ /**
+ * Sets the reading position in bits.
+ *
+ * @param position The new reading position in bits.
+ */
+ public void setPosition(int position) {
+ byteOffset = position / 8;
+ bitOffset = position - (byteOffset * 8);
+ assertValidOffset();
+ }
+
+ /**
+ * Returns the number of remaining bits.
+ */
+ public int bitsLeft() {
+ return (byteLimit - byteOffset) * 8 - bitOffset;
+ }
+
+ private void assertValidOffset() {
+ // It is fine for position to be at the end of the array, but no further.
+ Assertions.checkState(byteOffset >= 0
+ && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java
new file mode 100644
index 0000000000..bdd3e13b99
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java
@@ -0,0 +1,522 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+
+/** Utility methods for parsing Vorbis streams. */
+public final class VorbisUtil {
+
+ /** Vorbis comment header. */
+ public static final class CommentHeader {
+
+ public final String vendor;
+ public final String[] comments;
+ public final int length;
+
+ public CommentHeader(String vendor, String[] comments, int length) {
+ this.vendor = vendor;
+ this.comments = comments;
+ this.length = length;
+ }
+ }
+
+ /** Vorbis identification header. */
+ public static final class VorbisIdHeader {
+
+ public final long version;
+ public final int channels;
+ public final long sampleRate;
+ public final int bitrateMax;
+ public final int bitrateNominal;
+ public final int bitrateMin;
+ public final int blockSize0;
+ public final int blockSize1;
+ public final boolean framingFlag;
+ public final byte[] data;
+
+ public VorbisIdHeader(
+ long version,
+ int channels,
+ long sampleRate,
+ int bitrateMax,
+ int bitrateNominal,
+ int bitrateMin,
+ int blockSize0,
+ int blockSize1,
+ boolean framingFlag,
+ byte[] data) {
+ this.version = version;
+ this.channels = channels;
+ this.sampleRate = sampleRate;
+ this.bitrateMax = bitrateMax;
+ this.bitrateNominal = bitrateNominal;
+ this.bitrateMin = bitrateMin;
+ this.blockSize0 = blockSize0;
+ this.blockSize1 = blockSize1;
+ this.framingFlag = framingFlag;
+ this.data = data;
+ }
+
+ public int getApproximateBitrate() {
+ return bitrateNominal == 0 ? (bitrateMin + bitrateMax) / 2 : bitrateNominal;
+ }
+ }
+
+ /** Vorbis setup header modes. */
+ public static final class Mode {
+
+ public final boolean blockFlag;
+ public final int windowType;
+ public final int transformType;
+ public final int mapping;
+
+ public Mode(boolean blockFlag, int windowType, int transformType, int mapping) {
+ this.blockFlag = blockFlag;
+ this.windowType = windowType;
+ this.transformType = transformType;
+ this.mapping = mapping;
+ }
+ }
+
+ private static final String TAG = "VorbisUtil";
+
+ /**
+ * Returns ilog(x), which is the index of the highest set bit in {@code x}.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-1190009.2.1">
+ * Vorbis spec</a>
+ * @param x the value of which the ilog should be calculated.
+ * @return ilog(x)
+ */
+ public static int iLog(int x) {
+ int val = 0;
+ while (x > 0) {
+ val++;
+ x >>>= 1;
+ }
+ return val;
+ }
+
+ /**
+ * Reads a Vorbis identification header from {@code headerData}.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2">Vorbis
+ * spec/Identification header</a>
+ * @param headerData a {@link ParsableByteArray} wrapping the header data.
+ * @return a {@link VorbisUtil.VorbisIdHeader} with meta data.
+ * @throws ParserException thrown if invalid capture pattern is detected.
+ */
+ public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData)
+ throws ParserException {
+
+ verifyVorbisHeaderCapturePattern(0x01, headerData, false);
+
+ long version = headerData.readLittleEndianUnsignedInt();
+ int channels = headerData.readUnsignedByte();
+ long sampleRate = headerData.readLittleEndianUnsignedInt();
+ int bitrateMax = headerData.readLittleEndianInt();
+ int bitrateNominal = headerData.readLittleEndianInt();
+ int bitrateMin = headerData.readLittleEndianInt();
+
+ int blockSize = headerData.readUnsignedByte();
+ int blockSize0 = (int) Math.pow(2, blockSize & 0x0F);
+ int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4);
+
+ boolean framingFlag = (headerData.readUnsignedByte() & 0x01) > 0;
+ // raw data of Vorbis setup header has to be passed to decoder as CSD buffer #1
+ byte[] data = Arrays.copyOf(headerData.data, headerData.limit());
+
+ return new VorbisIdHeader(version, channels, sampleRate, bitrateMax, bitrateNominal, bitrateMin,
+ blockSize0, blockSize1, framingFlag, data);
+ }
+
+ /**
+ * Reads a Vorbis comment header.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3">Vorbis
+ * spec/Comment header</a>
+ * @param headerData A {@link ParsableByteArray} wrapping the header data.
+ * @return A {@link VorbisUtil.CommentHeader} with all the comments.
+ * @throws ParserException If an error occurs parsing the comment header.
+ */
+ public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData)
+ throws ParserException {
+ return readVorbisCommentHeader(
+ headerData, /* hasMetadataHeader= */ true, /* hasFramingBit= */ true);
+ }
+
+ /**
+ * Reads a Vorbis comment header.
+ *
+ * <p>The data provided may not contain the Vorbis metadata common header and the framing bit.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3">Vorbis
+ * spec/Comment header</a>
+ * @param headerData A {@link ParsableByteArray} wrapping the header data.
+ * @param hasMetadataHeader Whether the {@code headerData} contains a Vorbis metadata common
+ * header preceding the comment header.
+ * @param hasFramingBit Whether the {@code headerData} contains a framing bit.
+ * @return A {@link VorbisUtil.CommentHeader} with all the comments.
+ * @throws ParserException If an error occurs parsing the comment header.
+ */
+ public static CommentHeader readVorbisCommentHeader(
+ ParsableByteArray headerData, boolean hasMetadataHeader, boolean hasFramingBit)
+ throws ParserException {
+
+ if (hasMetadataHeader) {
+ verifyVorbisHeaderCapturePattern(/* headerType= */ 0x03, headerData, /* quiet= */ false);
+ }
+ int length = 7;
+
+ int len = (int) headerData.readLittleEndianUnsignedInt();
+ length += 4;
+ String vendor = headerData.readString(len);
+ length += vendor.length();
+
+ long commentListLen = headerData.readLittleEndianUnsignedInt();
+ String[] comments = new String[(int) commentListLen];
+ length += 4;
+ for (int i = 0; i < commentListLen; i++) {
+ len = (int) headerData.readLittleEndianUnsignedInt();
+ length += 4;
+ comments[i] = headerData.readString(len);
+ length += comments[i].length();
+ }
+ if (hasFramingBit && (headerData.readUnsignedByte() & 0x01) == 0) {
+ throw new ParserException("framing bit expected to be set");
+ }
+ length += 1;
+ return new CommentHeader(vendor, comments, length);
+ }
+
+ /**
+ * Verifies whether the next bytes in {@code header} are a Vorbis header of the given {@code
+ * headerType}.
+ *
+ * @param headerType the type of the header expected.
+ * @param header the alleged header bytes.
+ * @param quiet if {@code true} no exceptions are thrown. Instead {@code false} is returned.
+ * @return the number of bytes read.
+ * @throws ParserException thrown if header type or capture pattern is not as expected.
+ */
+ public static boolean verifyVorbisHeaderCapturePattern(
+ int headerType, ParsableByteArray header, boolean quiet) throws ParserException {
+ if (header.bytesLeft() < 7) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("too short header: " + header.bytesLeft());
+ }
+ }
+
+ if (header.readUnsignedByte() != headerType) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("expected header type " + Integer.toHexString(headerType));
+ }
+ }
+
+ if (!(header.readUnsignedByte() == 'v'
+ && header.readUnsignedByte() == 'o'
+ && header.readUnsignedByte() == 'r'
+ && header.readUnsignedByte() == 'b'
+ && header.readUnsignedByte() == 'i'
+ && header.readUnsignedByte() == 's')) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("expected characters 'vorbis'");
+ }
+ }
+ return true;
+ }
+
+ /**
+ * This method reads the modes which are located at the very end of the Vorbis setup header.
+ * That's why we need to partially decode or at least read the entire setup header to know where
+ * to start reading the modes.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-650004.2.4">Vorbis
+ * spec/Setup header</a>
+ * @param headerData a {@link ParsableByteArray} containing setup header data.
+ * @param channels the number of channels.
+ * @return an array of {@link Mode}s.
+ * @throws ParserException thrown if bit stream is invalid.
+ */
+ public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels)
+ throws ParserException {
+
+ verifyVorbisHeaderCapturePattern(0x05, headerData, false);
+
+ int numberOfBooks = headerData.readUnsignedByte() + 1;
+
+ VorbisBitArray bitArray = new VorbisBitArray(headerData.data);
+ bitArray.skipBits(headerData.getPosition() * 8);
+
+ for (int i = 0; i < numberOfBooks; i++) {
+ readBook(bitArray);
+ }
+
+ int timeCount = bitArray.readBits(6) + 1;
+ for (int i = 0; i < timeCount; i++) {
+ if (bitArray.readBits(16) != 0x00) {
+ throw new ParserException("placeholder of time domain transforms not zeroed out");
+ }
+ }
+ readFloors(bitArray);
+ readResidues(bitArray);
+ readMappings(channels, bitArray);
+
+ Mode[] modes = readModes(bitArray);
+ if (!bitArray.readBit()) {
+ throw new ParserException("framing bit after modes not set as expected");
+ }
+ return modes;
+ }
+
+ private static Mode[] readModes(VorbisBitArray bitArray) {
+ int modeCount = bitArray.readBits(6) + 1;
+ Mode[] modes = new Mode[modeCount];
+ for (int i = 0; i < modeCount; i++) {
+ boolean blockFlag = bitArray.readBit();
+ int windowType = bitArray.readBits(16);
+ int transformType = bitArray.readBits(16);
+ int mapping = bitArray.readBits(8);
+ modes[i] = new Mode(blockFlag, windowType, transformType, mapping);
+ }
+ return modes;
+ }
+
+ private static void readMappings(int channels, VorbisBitArray bitArray)
+ throws ParserException {
+ int mappingsCount = bitArray.readBits(6) + 1;
+ for (int i = 0; i < mappingsCount; i++) {
+ int mappingType = bitArray.readBits(16);
+ if (mappingType != 0) {
+ Log.e(TAG, "mapping type other than 0 not supported: " + mappingType);
+ continue;
+ }
+ int submaps;
+ if (bitArray.readBit()) {
+ submaps = bitArray.readBits(4) + 1;
+ } else {
+ submaps = 1;
+ }
+ int couplingSteps;
+ if (bitArray.readBit()) {
+ couplingSteps = bitArray.readBits(8) + 1;
+ for (int j = 0; j < couplingSteps; j++) {
+ bitArray.skipBits(iLog(channels - 1)); // magnitude
+ bitArray.skipBits(iLog(channels - 1)); // angle
+ }
+ } /*else {
+ couplingSteps = 0;
+ }*/
+ if (bitArray.readBits(2) != 0x00) {
+ throw new ParserException("to reserved bits must be zero after mapping coupling steps");
+ }
+ if (submaps > 1) {
+ for (int j = 0; j < channels; j++) {
+ bitArray.skipBits(4); // mappingMux
+ }
+ }
+ for (int j = 0; j < submaps; j++) {
+ bitArray.skipBits(8); // discard
+ bitArray.skipBits(8); // submapFloor
+ bitArray.skipBits(8); // submapResidue
+ }
+ }
+ }
+
+ private static void readResidues(VorbisBitArray bitArray) throws ParserException {
+ int residueCount = bitArray.readBits(6) + 1;
+ for (int i = 0; i < residueCount; i++) {
+ int residueType = bitArray.readBits(16);
+ if (residueType > 2) {
+ throw new ParserException("residueType greater than 2 is not decodable");
+ } else {
+ bitArray.skipBits(24); // begin
+ bitArray.skipBits(24); // end
+ bitArray.skipBits(24); // partitionSize (add one)
+ int classifications = bitArray.readBits(6) + 1;
+ bitArray.skipBits(8); // classbook
+ int[] cascade = new int[classifications];
+ for (int j = 0; j < classifications; j++) {
+ int highBits = 0;
+ int lowBits = bitArray.readBits(3);
+ if (bitArray.readBit()) {
+ highBits = bitArray.readBits(5);
+ }
+ cascade[j] = highBits * 8 + lowBits;
+ }
+ for (int j = 0; j < classifications; j++) {
+ for (int k = 0; k < 8; k++) {
+ if ((cascade[j] & (0x01 << k)) != 0) {
+ bitArray.skipBits(8); // discard
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static void readFloors(VorbisBitArray bitArray) throws ParserException {
+ int floorCount = bitArray.readBits(6) + 1;
+ for (int i = 0; i < floorCount; i++) {
+ int floorType = bitArray.readBits(16);
+ switch (floorType) {
+ case 0:
+ bitArray.skipBits(8); //order
+ bitArray.skipBits(16); // rate
+ bitArray.skipBits(16); // barkMapSize
+ bitArray.skipBits(6); // amplitudeBits
+ bitArray.skipBits(8); // amplitudeOffset
+ int floorNumberOfBooks = bitArray.readBits(4) + 1;
+ for (int j = 0; j < floorNumberOfBooks; j++) {
+ bitArray.skipBits(8);
+ }
+ break;
+ case 1:
+ int partitions = bitArray.readBits(5);
+ int maximumClass = -1;
+ int[] partitionClassList = new int[partitions];
+ for (int j = 0; j < partitions; j++) {
+ partitionClassList[j] = bitArray.readBits(4);
+ if (partitionClassList[j] > maximumClass) {
+ maximumClass = partitionClassList[j];
+ }
+ }
+ int[] classDimensions = new int[maximumClass + 1];
+ for (int j = 0; j < classDimensions.length; j++) {
+ classDimensions[j] = bitArray.readBits(3) + 1;
+ int classSubclasses = bitArray.readBits(2);
+ if (classSubclasses > 0) {
+ bitArray.skipBits(8); // classMasterbooks
+ }
+ for (int k = 0; k < (1 << classSubclasses); k++) {
+ bitArray.skipBits(8); // subclassBook (subtract 1)
+ }
+ }
+ bitArray.skipBits(2); // multiplier (add one)
+ int rangeBits = bitArray.readBits(4);
+ int count = 0;
+ for (int j = 0, k = 0; j < partitions; j++) {
+ int idx = partitionClassList[j];
+ count += classDimensions[idx];
+ for (; k < count; k++) {
+ bitArray.skipBits(rangeBits); // floorValue
+ }
+ }
+ break;
+ default:
+ throw new ParserException("floor type greater than 1 not decodable: " + floorType);
+ }
+ }
+ }
+
+ private static CodeBook readBook(VorbisBitArray bitArray) throws ParserException {
+ if (bitArray.readBits(24) != 0x564342) {
+ throw new ParserException("expected code book to start with [0x56, 0x43, 0x42] at "
+ + bitArray.getPosition());
+ }
+ int dimensions = bitArray.readBits(16);
+ int entries = bitArray.readBits(24);
+ long[] lengthMap = new long[entries];
+
+ boolean isOrdered = bitArray.readBit();
+ if (!isOrdered) {
+ boolean isSparse = bitArray.readBit();
+ for (int i = 0; i < lengthMap.length; i++) {
+ if (isSparse) {
+ if (bitArray.readBit()) {
+ lengthMap[i] = (long) (bitArray.readBits(5) + 1);
+ } else { // entry unused
+ lengthMap[i] = 0;
+ }
+ } else { // not sparse
+ lengthMap[i] = (long) (bitArray.readBits(5) + 1);
+ }
+ }
+ } else {
+ int length = bitArray.readBits(5) + 1;
+ for (int i = 0; i < lengthMap.length;) {
+ int num = bitArray.readBits(iLog(entries - i));
+ for (int j = 0; j < num && i < lengthMap.length; i++, j++) {
+ lengthMap[i] = length;
+ }
+ length++;
+ }
+ }
+
+ int lookupType = bitArray.readBits(4);
+ if (lookupType > 2) {
+ throw new ParserException("lookup type greater than 2 not decodable: " + lookupType);
+ } else if (lookupType == 1 || lookupType == 2) {
+ bitArray.skipBits(32); // minimumValue
+ bitArray.skipBits(32); // deltaValue
+ int valueBits = bitArray.readBits(4) + 1;
+ bitArray.skipBits(1); // sequenceP
+ long lookupValuesCount;
+ if (lookupType == 1) {
+ if (dimensions != 0) {
+ lookupValuesCount = mapType1QuantValues(entries, dimensions);
+ } else {
+ lookupValuesCount = 0;
+ }
+ } else {
+ lookupValuesCount = (long) entries * dimensions;
+ }
+ // discard (no decoding required yet)
+ bitArray.skipBits((int) (lookupValuesCount * valueBits));
+ }
+ return new CodeBook(dimensions, entries, lengthMap, lookupType, isOrdered);
+ }
+
+ /**
+ * @see <a href="http://svn.xiph.org/trunk/vorbis/lib/sharedbook.c">_book_maptype1_quantvals</a>
+ */
+ private static long mapType1QuantValues(long entries, long dimension) {
+ return (long) Math.floor(Math.pow(entries, 1.d / dimension));
+ }
+
+ private VorbisUtil() {
+ // Prevent instantiation.
+ }
+
+ private static final class CodeBook {
+
+ public final int dimensions;
+ public final int entries;
+ public final long[] lengthMap;
+ public final int lookupType;
+ public final boolean isOrdered;
+
+ public CodeBook(int dimensions, int entries, long[] lengthMap, int lookupType,
+ boolean isOrdered) {
+ this.dimensions = dimensions;
+ this.entries = entries;
+ this.lengthMap = lengthMap;
+ this.lookupType = lookupType;
+ this.isOrdered = isOrdered;
+ }
+
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java
new file mode 100644
index 0000000000..35f539a394
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.amr;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * Extracts data from the AMR containers format (either AMR or AMR-WB). This follows RFC-4867,
+ * section 5.
+ *
+ * <p>This extractor only supports single-channel AMR container formats.
+ */
+public final class AmrExtractor implements Extractor {
+
+ /** Factory for {@link AmrExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AmrExtractor()};
+
+ /**
+ * Flags controlling the behavior of the extractor. Possible flag value is {@link
+ * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING})
+ public @interface Flags {}
+ /**
+ * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would
+ * otherwise not be possible.
+ */
+ public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
+
+ /**
+ * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR
+ * narrow band.
+ */
+ private static final int[] frameSizeBytesByTypeNb = {
+ 13,
+ 14,
+ 16,
+ 18,
+ 20,
+ 21,
+ 27,
+ 32,
+ 6, // AMR SID
+ 7, // GSM-EFR SID
+ 6, // TDMA-EFR SID
+ 6, // PDC-EFR SID
+ 1, // Future use
+ 1, // Future use
+ 1, // Future use
+ 1 // No data
+ };
+
+ /**
+ * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR wide
+ * band.
+ */
+ private static final int[] frameSizeBytesByTypeWb = {
+ 18,
+ 24,
+ 33,
+ 37,
+ 41,
+ 47,
+ 51,
+ 59,
+ 61,
+ 6, // AMR-WB SID
+ 1, // Future use
+ 1, // Future use
+ 1, // Future use
+ 1, // Future use
+ 1, // speech lost
+ 1 // No data
+ };
+
+ private static final byte[] amrSignatureNb = Util.getUtf8Bytes("#!AMR\n");
+ private static final byte[] amrSignatureWb = Util.getUtf8Bytes("#!AMR-WB\n");
+
+ /** Theoretical maximum frame size for a AMR frame. */
+ private static final int MAX_FRAME_SIZE_BYTES = frameSizeBytesByTypeWb[8];
+ /**
+ * The required number of samples in the stream with same sample size to classify the stream as a
+ * constant-bitrate-stream.
+ */
+ private static final int NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD = 20;
+
+ private static final int SAMPLE_RATE_WB = 16_000;
+ private static final int SAMPLE_RATE_NB = 8_000;
+ private static final int SAMPLE_TIME_PER_FRAME_US = 20_000;
+
+ private final byte[] scratch;
+ private final @Flags int flags;
+
+ private boolean isWideBand;
+ private long currentSampleTimeUs;
+ private int currentSampleSize;
+ private int currentSampleBytesRemaining;
+ private boolean hasOutputSeekMap;
+ private long firstSamplePosition;
+ private int firstSampleSize;
+ private int numSamplesWithSameSize;
+ private long timeOffsetUs;
+
+ private ExtractorOutput extractorOutput;
+ private TrackOutput trackOutput;
+ @Nullable private SeekMap seekMap;
+ private boolean hasOutputFormat;
+
+ public AmrExtractor() {
+ this(/* flags= */ 0);
+ }
+
+ /** @param flags Flags that control the extractor's behavior. */
+ public AmrExtractor(@Flags int flags) {
+ this.flags = flags;
+ scratch = new byte[1];
+ firstSampleSize = C.LENGTH_UNSET;
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return readAmrHeader(input);
+ }
+
+ @Override
+ public void init(ExtractorOutput extractorOutput) {
+ this.extractorOutput = extractorOutput;
+ trackOutput = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);
+ extractorOutput.endTracks();
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ if (input.getPosition() == 0) {
+ if (!readAmrHeader(input)) {
+ throw new ParserException("Could not find AMR header.");
+ }
+ }
+ maybeOutputFormat();
+ int sampleReadResult = readSample(input);
+ maybeOutputSeekMap(input.getLength(), sampleReadResult);
+ return sampleReadResult;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ currentSampleTimeUs = 0;
+ currentSampleSize = 0;
+ currentSampleBytesRemaining = 0;
+ if (position != 0 && seekMap instanceof ConstantBitrateSeekMap) {
+ timeOffsetUs = ((ConstantBitrateSeekMap) seekMap).getTimeUsAtPosition(position);
+ } else {
+ timeOffsetUs = 0;
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ /* package */ static int frameSizeBytesByTypeNb(int frameType) {
+ return frameSizeBytesByTypeNb[frameType];
+ }
+
+ /* package */ static int frameSizeBytesByTypeWb(int frameType) {
+ return frameSizeBytesByTypeWb[frameType];
+ }
+
+ /* package */ static byte[] amrSignatureNb() {
+ return Arrays.copyOf(amrSignatureNb, amrSignatureNb.length);
+ }
+
+ /* package */ static byte[] amrSignatureWb() {
+ return Arrays.copyOf(amrSignatureWb, amrSignatureWb.length);
+ }
+
+ // Internal methods.
+
+ /**
+ * Peeks the AMR header from the beginning of the input, and consumes it if it exists.
+ *
+ * @param input The {@link ExtractorInput} from which data should be peeked/read.
+ * @return Whether the AMR header has been read.
+ */
+ private boolean readAmrHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (peekAmrSignature(input, amrSignatureNb)) {
+ isWideBand = false;
+ input.skipFully(amrSignatureNb.length);
+ return true;
+ } else if (peekAmrSignature(input, amrSignatureWb)) {
+ isWideBand = true;
+ input.skipFully(amrSignatureWb.length);
+ return true;
+ }
+ return false;
+ }
+
+ /** Peeks from the beginning of the input to see if the given AMR signature exists. */
+ private boolean peekAmrSignature(ExtractorInput input, byte[] amrSignature)
+ throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ byte[] header = new byte[amrSignature.length];
+ input.peekFully(header, 0, amrSignature.length);
+ return Arrays.equals(header, amrSignature);
+ }
+
+ private void maybeOutputFormat() {
+ if (!hasOutputFormat) {
+ hasOutputFormat = true;
+ String mimeType = isWideBand ? MimeTypes.AUDIO_AMR_WB : MimeTypes.AUDIO_AMR_NB;
+ int sampleRate = isWideBand ? SAMPLE_RATE_WB : SAMPLE_RATE_NB;
+ trackOutput.format(
+ Format.createAudioSampleFormat(
+ /* id= */ null,
+ mimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ MAX_FRAME_SIZE_BYTES,
+ /* channelCount= */ 1,
+ sampleRate,
+ /* pcmEncoding= */ Format.NO_VALUE,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null));
+ }
+ }
+
+ private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
+ if (currentSampleBytesRemaining == 0) {
+ try {
+ currentSampleSize = peekNextSampleSize(extractorInput);
+ } catch (EOFException e) {
+ return RESULT_END_OF_INPUT;
+ }
+ currentSampleBytesRemaining = currentSampleSize;
+ if (firstSampleSize == C.LENGTH_UNSET) {
+ firstSamplePosition = extractorInput.getPosition();
+ firstSampleSize = currentSampleSize;
+ }
+ if (firstSampleSize == currentSampleSize) {
+ numSamplesWithSameSize++;
+ }
+ }
+
+ int bytesAppended =
+ trackOutput.sampleData(
+ extractorInput, currentSampleBytesRemaining, /* allowEndOfInput= */ true);
+ if (bytesAppended == C.RESULT_END_OF_INPUT) {
+ return RESULT_END_OF_INPUT;
+ }
+ currentSampleBytesRemaining -= bytesAppended;
+ if (currentSampleBytesRemaining > 0) {
+ return RESULT_CONTINUE;
+ }
+
+ trackOutput.sampleMetadata(
+ timeOffsetUs + currentSampleTimeUs,
+ C.BUFFER_FLAG_KEY_FRAME,
+ currentSampleSize,
+ /* offset= */ 0,
+ /* encryptionData= */ null);
+ currentSampleTimeUs += SAMPLE_TIME_PER_FRAME_US;
+ return RESULT_CONTINUE;
+ }
+
+ private int peekNextSampleSize(ExtractorInput extractorInput)
+ throws IOException, InterruptedException {
+ extractorInput.resetPeekPosition();
+ extractorInput.peekFully(scratch, /* offset= */ 0, /* length= */ 1);
+
+ byte frameHeader = scratch[0];
+ if ((frameHeader & 0x83) > 0) {
+ // The padding bits are at bit-1 positions in the following pattern: 1000 0011
+ // Padding bits must be 0.
+ throw new ParserException("Invalid padding bits for frame header " + frameHeader);
+ }
+
+ int frameType = (frameHeader >> 3) & 0x0f;
+ return getFrameSizeInBytes(frameType);
+ }
+
+ private int getFrameSizeInBytes(int frameType) throws ParserException {
+ if (!isValidFrameType(frameType)) {
+ throw new ParserException(
+ "Illegal AMR " + (isWideBand ? "WB" : "NB") + " frame type " + frameType);
+ }
+
+ return isWideBand ? frameSizeBytesByTypeWb[frameType] : frameSizeBytesByTypeNb[frameType];
+ }
+
+ private boolean isValidFrameType(int frameType) {
+ return frameType >= 0
+ && frameType <= 15
+ && (isWideBandValidFrameType(frameType) || isNarrowBandValidFrameType(frameType));
+ }
+
+ private boolean isWideBandValidFrameType(int frameType) {
+ // For wide band, type 10-13 are for future use.
+ return isWideBand && (frameType < 10 || frameType > 13);
+ }
+
+ private boolean isNarrowBandValidFrameType(int frameType) {
+ // For narrow band, type 12-14 are for future use.
+ return !isWideBand && (frameType < 12 || frameType > 14);
+ }
+
+ private void maybeOutputSeekMap(long inputLength, int sampleReadResult) {
+ if (hasOutputSeekMap) {
+ return;
+ }
+
+ if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) == 0
+ || inputLength == C.LENGTH_UNSET
+ || (firstSampleSize != C.LENGTH_UNSET && firstSampleSize != currentSampleSize)) {
+ seekMap = new SeekMap.Unseekable(C.TIME_UNSET);
+ extractorOutput.seekMap(seekMap);
+ hasOutputSeekMap = true;
+ } else if (numSamplesWithSameSize >= NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD
+ || sampleReadResult == RESULT_END_OF_INPUT) {
+ seekMap = getConstantBitrateSeekMap(inputLength);
+ extractorOutput.seekMap(seekMap);
+ hasOutputSeekMap = true;
+ }
+ }
+
+ private SeekMap getConstantBitrateSeekMap(long inputLength) {
+ int bitrate = getBitrateFromFrameSize(firstSampleSize, SAMPLE_TIME_PER_FRAME_US);
+ return new ConstantBitrateSeekMap(inputLength, firstSamplePosition, bitrate, firstSampleSize);
+ }
+
+ /**
+ * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds.
+ *
+ * @param frameSize The size of each frame in the stream.
+ * @param durationUsPerFrame The duration of the given frame in microseconds.
+ * @return The stream bitrate.
+ */
+ private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) {
+ return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java
new file mode 100644
index 0000000000..d13b1f394d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flac;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.BinarySearchSeeker;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata;
+import java.io.IOException;
+
+/**
+ * A {@link SeekMap} implementation for FLAC stream using binary search.
+ *
+ * <p>This seeker performs seeking by using binary search within the stream, until it finds the
+ * frame that contains the target sample.
+ */
+/* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker {
+
+ /**
+ * Creates a {@link FlacBinarySearchSeeker}.
+ *
+ * @param flacStreamMetadata The stream metadata.
+ * @param frameStartMarker The frame start marker, consisting of the 2 bytes by which every frame
+ * in the stream must start.
+ * @param firstFramePosition The byte offset of the first frame in the stream.
+ * @param inputLength The length of the stream in bytes.
+ */
+ public FlacBinarySearchSeeker(
+ FlacStreamMetadata flacStreamMetadata,
+ int frameStartMarker,
+ long firstFramePosition,
+ long inputLength) {
+ super(
+ /* seekTimestampConverter= */ flacStreamMetadata::getSampleNumber,
+ new FlacTimestampSeeker(flacStreamMetadata, frameStartMarker),
+ flacStreamMetadata.getDurationUs(),
+ /* floorTimePosition= */ 0,
+ /* ceilingTimePosition= */ flacStreamMetadata.totalSamples,
+ /* floorBytePosition= */ firstFramePosition,
+ /* ceilingBytePosition= */ inputLength,
+ /* approxBytesPerFrame= */ flacStreamMetadata.getApproxBytesPerFrame(),
+ /* minimumSearchRange= */ Math.max(
+ FlacConstants.MIN_FRAME_HEADER_SIZE, flacStreamMetadata.minFrameSize));
+ }
+
+ private static final class FlacTimestampSeeker implements TimestampSeeker {
+
+ private final FlacStreamMetadata flacStreamMetadata;
+ private final int frameStartMarker;
+ private final SampleNumberHolder sampleNumberHolder;
+
+ private FlacTimestampSeeker(FlacStreamMetadata flacStreamMetadata, int frameStartMarker) {
+ this.flacStreamMetadata = flacStreamMetadata;
+ this.frameStartMarker = frameStartMarker;
+ sampleNumberHolder = new SampleNumberHolder();
+ }
+
+ @Override
+ public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleNumber)
+ throws IOException, InterruptedException {
+ long searchPosition = input.getPosition();
+
+ // Find left frame.
+ long leftFrameFirstSampleNumber = findNextFrame(input);
+ long leftFramePosition = input.getPeekPosition();
+
+ input.advancePeekPosition(
+ Math.max(FlacConstants.MIN_FRAME_HEADER_SIZE, flacStreamMetadata.minFrameSize));
+
+ // Find right frame.
+ long rightFrameFirstSampleNumber = findNextFrame(input);
+ long rightFramePosition = input.getPeekPosition();
+
+ if (leftFrameFirstSampleNumber <= targetSampleNumber
+ && rightFrameFirstSampleNumber > targetSampleNumber) {
+ return TimestampSearchResult.targetFoundResult(leftFramePosition);
+ } else if (rightFrameFirstSampleNumber <= targetSampleNumber) {
+ return TimestampSearchResult.underestimatedResult(
+ rightFrameFirstSampleNumber, rightFramePosition);
+ } else {
+ return TimestampSearchResult.overestimatedResult(
+ leftFrameFirstSampleNumber, searchPosition);
+ }
+ }
+
+ /**
+ * Searches for the next frame in {@code input}.
+ *
+ * <p>The peek position is advanced to the start of the found frame, or at the end of the stream
+ * if no frame was found.
+ *
+ * @param input The input from which to search (starting from the peek position).
+ * @return The number of the first sample in the found frame, or the total number of samples in
+ * the stream if no frame was found.
+ * @throws IOException If peeking from the input fails. In this case, there is no guarantee on
+ * the peek position.
+ * @throws InterruptedException If interrupted while peeking from input. In this case, there is
+ * no guarantee on the peek position.
+ */
+ private long findNextFrame(ExtractorInput input) throws IOException, InterruptedException {
+ while (input.getPeekPosition() < input.getLength() - FlacConstants.MIN_FRAME_HEADER_SIZE
+ && !FlacFrameReader.checkFrameHeaderFromPeek(
+ input, flacStreamMetadata, frameStartMarker, sampleNumberHolder)) {
+ input.advancePeekPosition(1);
+ }
+
+ if (input.getPeekPosition() >= input.getLength() - FlacConstants.MIN_FRAME_HEADER_SIZE) {
+ input.advancePeekPosition((int) (input.getLength() - input.getPeekPosition()));
+ return flacStreamMetadata.totalSamples;
+ }
+
+ return sampleNumberHolder.sampleNumber;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java
new file mode 100644
index 0000000000..fa997001e8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flac;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacMetadataReader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * Extracts data from FLAC container format.
+ *
+ * <p>The format specification can be found at https://xiph.org/flac/format.html.
+ */
+public final class FlacExtractor implements Extractor {
+
+ /** Factory for {@link FlacExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};
+
+ /**
+ * Flags controlling the behavior of the extractor. Possible flag value is {@link
+ * #FLAG_DISABLE_ID3_METADATA}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {FLAG_DISABLE_ID3_METADATA})
+ public @interface Flags {}
+
+ /**
+ * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
+ * required.
+ */
+ public static final int FLAG_DISABLE_ID3_METADATA = 1;
+
+ /** Parser state. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATE_READ_ID3_METADATA,
+ STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES,
+ STATE_READ_STREAM_MARKER,
+ STATE_READ_METADATA_BLOCKS,
+ STATE_GET_FRAME_START_MARKER,
+ STATE_READ_FRAMES
+ })
+ private @interface State {}
+
+ private static final int STATE_READ_ID3_METADATA = 0;
+ private static final int STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES = 1;
+ private static final int STATE_READ_STREAM_MARKER = 2;
+ private static final int STATE_READ_METADATA_BLOCKS = 3;
+ private static final int STATE_GET_FRAME_START_MARKER = 4;
+ private static final int STATE_READ_FRAMES = 5;
+
+ /** Arbitrary buffer length of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */
+ private static final int BUFFER_LENGTH = 32 * 1024;
+
+ /** Value of an unknown sample number. */
+ private static final int SAMPLE_NUMBER_UNKNOWN = -1;
+
+ private final byte[] streamMarkerAndInfoBlock;
+ private final ParsableByteArray buffer;
+ private final boolean id3MetadataDisabled;
+
+ private final SampleNumberHolder sampleNumberHolder;
+
+ @MonotonicNonNull private ExtractorOutput extractorOutput;
+ @MonotonicNonNull private TrackOutput trackOutput;
+
+ private @State int state;
+ @Nullable private Metadata id3Metadata;
+ @MonotonicNonNull private FlacStreamMetadata flacStreamMetadata;
+ private int minFrameSize;
+ private int frameStartMarker;
+ @MonotonicNonNull private FlacBinarySearchSeeker binarySearchSeeker;
+ private int currentFrameBytesWritten;
+ private long currentFrameFirstSampleNumber;
+
+ /** Constructs an instance with {@code flags = 0}. */
+ public FlacExtractor() {
+ this(/* flags= */ 0);
+ }
+
+ /**
+ * Constructs an instance.
+ *
+ * @param flags Flags that control the extractor's behavior. Possible flags are described by
+ * {@link Flags}.
+ */
+ public FlacExtractor(int flags) {
+ streamMarkerAndInfoBlock =
+ new byte[FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE];
+ buffer = new ParsableByteArray(new byte[BUFFER_LENGTH], /* limit= */ 0);
+ id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
+ sampleNumberHolder = new SampleNumberHolder();
+ state = STATE_READ_ID3_METADATA;
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false);
+ return FlacMetadataReader.checkAndPeekStreamMarker(input);
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);
+ output.endTracks();
+ }
+
+ @Override
+ public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ switch (state) {
+ case STATE_READ_ID3_METADATA:
+ readId3Metadata(input);
+ return Extractor.RESULT_CONTINUE;
+ case STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES:
+ getStreamMarkerAndInfoBlockBytes(input);
+ return Extractor.RESULT_CONTINUE;
+ case STATE_READ_STREAM_MARKER:
+ readStreamMarker(input);
+ return Extractor.RESULT_CONTINUE;
+ case STATE_READ_METADATA_BLOCKS:
+ readMetadataBlocks(input);
+ return Extractor.RESULT_CONTINUE;
+ case STATE_GET_FRAME_START_MARKER:
+ getFrameStartMarker(input);
+ return Extractor.RESULT_CONTINUE;
+ case STATE_READ_FRAMES:
+ return readFrames(input, seekPosition);
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ if (position == 0) {
+ state = STATE_READ_ID3_METADATA;
+ } else if (binarySearchSeeker != null) {
+ binarySearchSeeker.setSeekTargetUs(timeUs);
+ }
+ currentFrameFirstSampleNumber = timeUs == 0 ? 0 : SAMPLE_NUMBER_UNKNOWN;
+ currentFrameBytesWritten = 0;
+ buffer.reset();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing.
+ }
+
+ // Private methods.
+
+ private void readId3Metadata(ExtractorInput input) throws IOException, InterruptedException {
+ id3Metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ !id3MetadataDisabled);
+ state = STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES;
+ }
+
+ private void getStreamMarkerAndInfoBlockBytes(ExtractorInput input)
+ throws IOException, InterruptedException {
+ input.peekFully(streamMarkerAndInfoBlock, 0, streamMarkerAndInfoBlock.length);
+ input.resetPeekPosition();
+ state = STATE_READ_STREAM_MARKER;
+ }
+
+ private void readStreamMarker(ExtractorInput input) throws IOException, InterruptedException {
+ FlacMetadataReader.readStreamMarker(input);
+ state = STATE_READ_METADATA_BLOCKS;
+ }
+
+ private void readMetadataBlocks(ExtractorInput input) throws IOException, InterruptedException {
+ boolean isLastMetadataBlock = false;
+ FlacMetadataReader.FlacStreamMetadataHolder metadataHolder =
+ new FlacMetadataReader.FlacStreamMetadataHolder(flacStreamMetadata);
+ while (!isLastMetadataBlock) {
+ isLastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, metadataHolder);
+ // Save the current metadata in case an exception occurs.
+ flacStreamMetadata = castNonNull(metadataHolder.flacStreamMetadata);
+ }
+
+ Assertions.checkNotNull(flacStreamMetadata);
+ minFrameSize = Math.max(flacStreamMetadata.minFrameSize, FlacConstants.MIN_FRAME_HEADER_SIZE);
+ castNonNull(trackOutput)
+ .format(flacStreamMetadata.getFormat(streamMarkerAndInfoBlock, id3Metadata));
+
+ state = STATE_GET_FRAME_START_MARKER;
+ }
+
+ private void getFrameStartMarker(ExtractorInput input) throws IOException, InterruptedException {
+ frameStartMarker = FlacMetadataReader.getFrameStartMarker(input);
+ castNonNull(extractorOutput)
+ .seekMap(
+ getSeekMap(
+ /* firstFramePosition= */ input.getPosition(),
+ /* streamLength= */ input.getLength()));
+
+ state = STATE_READ_FRAMES;
+ }
+
+ private @ReadResult int readFrames(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ Assertions.checkNotNull(trackOutput);
+ Assertions.checkNotNull(flacStreamMetadata);
+
+ // Handle pending binary search seek if necessary.
+ if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
+ return binarySearchSeeker.handlePendingSeek(input, seekPosition);
+ }
+
+ // Set current frame first sample number if it became unknown after seeking.
+ if (currentFrameFirstSampleNumber == SAMPLE_NUMBER_UNKNOWN) {
+ currentFrameFirstSampleNumber =
+ FlacFrameReader.getFirstSampleNumber(input, flacStreamMetadata);
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ // Copy more bytes into the buffer.
+ int currentLimit = buffer.limit();
+ boolean foundEndOfInput = false;
+ if (currentLimit < BUFFER_LENGTH) {
+ int bytesRead =
+ input.read(
+ buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit);
+ foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT;
+ if (!foundEndOfInput) {
+ buffer.setLimit(currentLimit + bytesRead);
+ } else if (buffer.bytesLeft() == 0) {
+ outputSampleMetadata();
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ }
+
+ // Search for a frame.
+ int positionBeforeFindingAFrame = buffer.getPosition();
+
+ // Skip frame search on the bytes within the minimum frame size.
+ if (currentFrameBytesWritten < minFrameSize) {
+ buffer.skipBytes(Math.min(minFrameSize - currentFrameBytesWritten, buffer.bytesLeft()));
+ }
+
+ long nextFrameFirstSampleNumber = findFrame(buffer, foundEndOfInput);
+ int numberOfFrameBytes = buffer.getPosition() - positionBeforeFindingAFrame;
+ buffer.setPosition(positionBeforeFindingAFrame);
+ trackOutput.sampleData(buffer, numberOfFrameBytes);
+ currentFrameBytesWritten += numberOfFrameBytes;
+
+ // Frame found.
+ if (nextFrameFirstSampleNumber != SAMPLE_NUMBER_UNKNOWN) {
+ outputSampleMetadata();
+ currentFrameBytesWritten = 0;
+ currentFrameFirstSampleNumber = nextFrameFirstSampleNumber;
+ }
+
+ if (buffer.bytesLeft() < FlacConstants.MAX_FRAME_HEADER_SIZE) {
+ // The next frame header may not fit in the rest of the buffer, so put the trailing bytes at
+ // the start of the buffer, and reset the position and limit.
+ System.arraycopy(
+ buffer.data, buffer.getPosition(), buffer.data, /* destPos= */ 0, buffer.bytesLeft());
+ buffer.reset(buffer.bytesLeft());
+ }
+
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private SeekMap getSeekMap(long firstFramePosition, long streamLength) {
+ Assertions.checkNotNull(flacStreamMetadata);
+ if (flacStreamMetadata.seekTable != null) {
+ return new FlacSeekTableSeekMap(flacStreamMetadata, firstFramePosition);
+ } else if (streamLength != C.LENGTH_UNSET && flacStreamMetadata.totalSamples > 0) {
+ binarySearchSeeker =
+ new FlacBinarySearchSeeker(
+ flacStreamMetadata, frameStartMarker, firstFramePosition, streamLength);
+ return binarySearchSeeker.getSeekMap();
+ } else {
+ return new SeekMap.Unseekable(flacStreamMetadata.getDurationUs());
+ }
+ }
+
+ /**
+ * Searches for the start of a frame in {@code data}.
+ *
+ * <ul>
+ * <li>If the search is successful, the position is set to the start of the found frame.
+ * <li>Otherwise, the position is set to the first unsearched byte.
+ * </ul>
+ *
+ * @param data The array to be searched.
+ * @param foundEndOfInput If the end of input was met when filling in the {@code data}.
+ * @return The number of the first sample in the frame found, or {@code SAMPLE_NUMBER_UNKNOWN} if
+ * the search was not successful.
+ */
+ private long findFrame(ParsableByteArray data, boolean foundEndOfInput) {
+ Assertions.checkNotNull(flacStreamMetadata);
+
+ int frameOffset = data.getPosition();
+ while (frameOffset <= data.limit() - FlacConstants.MAX_FRAME_HEADER_SIZE) {
+ data.setPosition(frameOffset);
+ if (FlacFrameReader.checkAndReadFrameHeader(
+ data, flacStreamMetadata, frameStartMarker, sampleNumberHolder)) {
+ data.setPosition(frameOffset);
+ return sampleNumberHolder.sampleNumber;
+ }
+ frameOffset++;
+ }
+
+ if (foundEndOfInput) {
+ // Verify whether there is a frame of size < MAX_FRAME_HEADER_SIZE at the end of the stream by
+ // checking at every position at a distance between MAX_FRAME_HEADER_SIZE and minFrameSize
+ // from the buffer limit if it corresponds to a valid frame header.
+ // At every offset, the different possibilities are:
+ // 1. The current offset indicates the start of a valid frame header. In this case, consider
+ // that a frame has been found and stop searching.
+ // 2. A frame starting at the current offset would be invalid. In this case, keep looking for
+ // a valid frame header.
+ // 3. The current offset could be the start of a valid frame header, but there is not enough
+ // bytes remaining to complete the header. As the end of the file has been reached, this
+ // means that the current offset does not correspond to a new frame and that the last bytes
+ // of the last frame happen to be a valid partial frame header. This case can occur in two
+ // ways:
+ // 3.1. An attempt to read past the buffer is made when reading the potential frame header.
+ // 3.2. Reading the potential frame header does not exceed the buffer size, but exceeds the
+ // buffer limit.
+ // Note that the third case is very unlikely. It never happens if the end of the input has not
+ // been reached as it is always made sure that the buffer has at least MAX_FRAME_HEADER_SIZE
+ // bytes available when reading a potential frame header.
+ while (frameOffset <= data.limit() - minFrameSize) {
+ data.setPosition(frameOffset);
+ boolean frameFound;
+ try {
+ frameFound =
+ FlacFrameReader.checkAndReadFrameHeader(
+ data, flacStreamMetadata, frameStartMarker, sampleNumberHolder);
+ } catch (IndexOutOfBoundsException e) {
+ // Case 3.1.
+ frameFound = false;
+ }
+ if (data.getPosition() > data.limit()) {
+ // TODO: Remove (and update above comments) once [Internal ref: b/147657250] is fixed.
+ // Case 3.2.
+ frameFound = false;
+ }
+ if (frameFound) {
+ // Case 1.
+ data.setPosition(frameOffset);
+ return sampleNumberHolder.sampleNumber;
+ }
+ frameOffset++;
+ }
+ // The end of the frame is the end of the file.
+ data.setPosition(data.limit());
+ } else {
+ data.setPosition(frameOffset);
+ }
+
+ return SAMPLE_NUMBER_UNKNOWN;
+ }
+
+ private void outputSampleMetadata() {
+ long timeUs =
+ currentFrameFirstSampleNumber
+ * C.MICROS_PER_SECOND
+ / castNonNull(flacStreamMetadata).sampleRate;
+ castNonNull(trackOutput)
+ .sampleMetadata(
+ timeUs,
+ C.BUFFER_FLAG_KEY_FRAME,
+ currentFrameBytesWritten,
+ /* offset= */ 0,
+ /* encryptionData= */ null);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java
new file mode 100644
index 0000000000..54dbaec003
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java
@@ -0,0 +1,130 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv;
+
+import android.util.Pair;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Collections;
+
+/**
+ * Parses audio tags from an FLV stream and extracts AAC frames.
+ */
+/* package */ final class AudioTagPayloadReader extends TagPayloadReader {
+
+ private static final int AUDIO_FORMAT_MP3 = 2;
+ private static final int AUDIO_FORMAT_ALAW = 7;
+ private static final int AUDIO_FORMAT_ULAW = 8;
+ private static final int AUDIO_FORMAT_AAC = 10;
+
+ private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0;
+ private static final int AAC_PACKET_TYPE_AAC_RAW = 1;
+
+ private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] {5512, 11025, 22050, 44100};
+
+ // State variables
+ private boolean hasParsedAudioDataHeader;
+ private boolean hasOutputFormat;
+ private int audioFormat;
+
+ public AudioTagPayloadReader(TrackOutput output) {
+ super(output);
+ }
+
+ @Override
+ public void seek() {
+ // Do nothing.
+ }
+
+ @Override
+ protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException {
+ if (!hasParsedAudioDataHeader) {
+ int header = data.readUnsignedByte();
+ audioFormat = (header >> 4) & 0x0F;
+ if (audioFormat == AUDIO_FORMAT_MP3) {
+ int sampleRateIndex = (header >> 2) & 0x03;
+ int sampleRate = AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex];
+ Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_MPEG, null,
+ Format.NO_VALUE, Format.NO_VALUE, 1, sampleRate, null, null, 0, null);
+ output.format(format);
+ hasOutputFormat = true;
+ } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) {
+ String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW
+ : MimeTypes.AUDIO_MLAW;
+ Format format =
+ Format.createAudioSampleFormat(
+ /* id= */ null,
+ /* sampleMimeType= */ type,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ /* maxInputSize= */ Format.NO_VALUE,
+ /* channelCount= */ 1,
+ /* sampleRate= */ 8000,
+ /* pcmEncoding= */ Format.NO_VALUE,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null);
+ output.format(format);
+ hasOutputFormat = true;
+ } else if (audioFormat != AUDIO_FORMAT_AAC) {
+ throw new UnsupportedFormatException("Audio format not supported: " + audioFormat);
+ }
+ hasParsedAudioDataHeader = true;
+ } else {
+ // Skip header if it was parsed previously.
+ data.skipBytes(1);
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
+ if (audioFormat == AUDIO_FORMAT_MP3) {
+ int sampleSize = data.bytesLeft();
+ output.sampleData(data, sampleSize);
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ return true;
+ } else {
+ int packetType = data.readUnsignedByte();
+ if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {
+ // Parse the sequence header.
+ byte[] audioSpecificConfig = new byte[data.bytesLeft()];
+ data.readBytes(audioSpecificConfig, 0, audioSpecificConfig.length);
+ Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig(
+ audioSpecificConfig);
+ Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null,
+ Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first,
+ Collections.singletonList(audioSpecificConfig), null, 0, null);
+ output.format(format);
+ hasOutputFormat = true;
+ return false;
+ } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) {
+ int sampleSize = data.bytesLeft();
+ output.sampleData(data, sampleSize);
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
new file mode 100644
index 0000000000..a7438b190f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
@@ -0,0 +1,308 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv;
+
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Extracts data from the FLV container format.
+ */
+public final class FlvExtractor implements Extractor {
+
+ /** Factory for {@link FlvExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlvExtractor()};
+
+ /** Extractor states. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATE_READING_FLV_HEADER,
+ STATE_SKIPPING_TO_TAG_HEADER,
+ STATE_READING_TAG_HEADER,
+ STATE_READING_TAG_DATA
+ })
+ private @interface States {}
+
+ private static final int STATE_READING_FLV_HEADER = 1;
+ private static final int STATE_SKIPPING_TO_TAG_HEADER = 2;
+ private static final int STATE_READING_TAG_HEADER = 3;
+ private static final int STATE_READING_TAG_DATA = 4;
+
+ // Header sizes.
+ private static final int FLV_HEADER_SIZE = 9;
+ private static final int FLV_TAG_HEADER_SIZE = 11;
+
+ // Tag types.
+ private static final int TAG_TYPE_AUDIO = 8;
+ private static final int TAG_TYPE_VIDEO = 9;
+ private static final int TAG_TYPE_SCRIPT_DATA = 18;
+
+ // FLV container identifier.
+ private static final int FLV_TAG = 0x00464c56;
+
+ private final ParsableByteArray scratch;
+ private final ParsableByteArray headerBuffer;
+ private final ParsableByteArray tagHeaderBuffer;
+ private final ParsableByteArray tagData;
+ private final ScriptTagPayloadReader metadataReader;
+
+ private ExtractorOutput extractorOutput;
+ private @States int state;
+ private boolean outputFirstSample;
+ private long mediaTagTimestampOffsetUs;
+ private int bytesToNextTagHeader;
+ private int tagType;
+ private int tagDataSize;
+ private long tagTimestampUs;
+ private boolean outputSeekMap;
+ private AudioTagPayloadReader audioReader;
+ private VideoTagPayloadReader videoReader;
+
+ public FlvExtractor() {
+ scratch = new ParsableByteArray(4);
+ headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE);
+ tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);
+ tagData = new ParsableByteArray();
+ metadataReader = new ScriptTagPayloadReader();
+ state = STATE_READING_FLV_HEADER;
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ // Check if file starts with "FLV" tag
+ input.peekFully(scratch.data, 0, 3);
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != FLV_TAG) {
+ return false;
+ }
+
+ // Checking reserved flags are set to 0
+ input.peekFully(scratch.data, 0, 2);
+ scratch.setPosition(0);
+ if ((scratch.readUnsignedShort() & 0xFA) != 0) {
+ return false;
+ }
+
+ // Read data offset
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+ int dataOffset = scratch.readInt();
+
+ input.resetPeekPosition();
+ input.advancePeekPosition(dataOffset);
+
+ // Checking first "previous tag size" is set to 0
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+
+ return scratch.readInt() == 0;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.extractorOutput = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ state = STATE_READING_FLV_HEADER;
+ outputFirstSample = false;
+ bytesToNextTagHeader = 0;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
+ InterruptedException {
+ while (true) {
+ switch (state) {
+ case STATE_READING_FLV_HEADER:
+ if (!readFlvHeader(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_SKIPPING_TO_TAG_HEADER:
+ skipToTagHeader(input);
+ break;
+ case STATE_READING_TAG_HEADER:
+ if (!readTagHeader(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_TAG_DATA:
+ if (readTagData(input)) {
+ return RESULT_CONTINUE;
+ }
+ break;
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ /**
+ * Reads an FLV container header from the provided {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return True if header was read successfully. False if the end of stream was reached.
+ * @throws IOException If an error occurred reading or parsing data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private boolean readFlvHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (!input.readFully(headerBuffer.data, 0, FLV_HEADER_SIZE, true)) {
+ // We've reached the end of the stream.
+ return false;
+ }
+
+ headerBuffer.setPosition(0);
+ headerBuffer.skipBytes(4);
+ int flags = headerBuffer.readUnsignedByte();
+ boolean hasAudio = (flags & 0x04) != 0;
+ boolean hasVideo = (flags & 0x01) != 0;
+ if (hasAudio && audioReader == null) {
+ audioReader = new AudioTagPayloadReader(
+ extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO));
+ }
+ if (hasVideo && videoReader == null) {
+ videoReader = new VideoTagPayloadReader(
+ extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO));
+ }
+ extractorOutput.endTracks();
+
+ // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size.
+ bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4;
+ state = STATE_SKIPPING_TO_TAG_HEADER;
+ return true;
+ }
+
+ /**
+ * Skips over data to reach the next tag header.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @throws IOException If an error occurred skipping data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException {
+ input.skipFully(bytesToNextTagHeader);
+ bytesToNextTagHeader = 0;
+ state = STATE_READING_TAG_HEADER;
+ }
+
+ /**
+ * Reads a tag header from the provided {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return True if tag header was read successfully. Otherwise, false.
+ * @throws IOException If an error occurred reading or parsing data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (!input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE, true)) {
+ // We've reached the end of the stream.
+ return false;
+ }
+
+ tagHeaderBuffer.setPosition(0);
+ tagType = tagHeaderBuffer.readUnsignedByte();
+ tagDataSize = tagHeaderBuffer.readUnsignedInt24();
+ tagTimestampUs = tagHeaderBuffer.readUnsignedInt24();
+ tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L;
+ tagHeaderBuffer.skipBytes(3); // streamId
+ state = STATE_READING_TAG_DATA;
+ return true;
+ }
+
+ /**
+ * Reads the body of a tag from the provided {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return True if the data was consumed by a reader. False if it was skipped.
+ * @throws IOException If an error occurred reading or parsing data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException {
+ boolean wasConsumed = true;
+ boolean wasSampleOutput = false;
+ long timestampUs = getCurrentTimestampUs();
+ if (tagType == TAG_TYPE_AUDIO && audioReader != null) {
+ ensureReadyForMediaOutput();
+ wasSampleOutput = audioReader.consume(prepareTagData(input), timestampUs);
+ } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {
+ ensureReadyForMediaOutput();
+ wasSampleOutput = videoReader.consume(prepareTagData(input), timestampUs);
+ } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) {
+ wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs);
+ long durationUs = metadataReader.getDurationUs();
+ if (durationUs != C.TIME_UNSET) {
+ extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
+ outputSeekMap = true;
+ }
+ } else {
+ input.skipFully(tagDataSize);
+ wasConsumed = false;
+ }
+ if (!outputFirstSample && wasSampleOutput) {
+ outputFirstSample = true;
+ mediaTagTimestampOffsetUs =
+ metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0;
+ }
+ bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header.
+ state = STATE_SKIPPING_TO_TAG_HEADER;
+ return wasConsumed;
+ }
+
+ private ParsableByteArray prepareTagData(ExtractorInput input) throws IOException,
+ InterruptedException {
+ if (tagDataSize > tagData.capacity()) {
+ tagData.reset(new byte[Math.max(tagData.capacity() * 2, tagDataSize)], 0);
+ } else {
+ tagData.setPosition(0);
+ }
+ tagData.setLimit(tagDataSize);
+ input.readFully(tagData.data, 0, tagDataSize);
+ return tagData;
+ }
+
+ private void ensureReadyForMediaOutput() {
+ if (!outputSeekMap) {
+ extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ outputSeekMap = true;
+ }
+ }
+
+ private long getCurrentTimestampUs() {
+ return outputFirstSample
+ ? (mediaTagTimestampOffsetUs + tagTimestampUs)
+ : (metadataReader.getDurationUs() == C.TIME_UNSET ? 0 : tagTimestampUs);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java
new file mode 100644
index 0000000000..1494bf1c2e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java
@@ -0,0 +1,227 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Parses Script Data tags from an FLV stream and extracts metadata information.
+ */
+/* package */ final class ScriptTagPayloadReader extends TagPayloadReader {
+
+ private static final String NAME_METADATA = "onMetaData";
+ private static final String KEY_DURATION = "duration";
+
+ // AMF object types
+ private static final int AMF_TYPE_NUMBER = 0;
+ private static final int AMF_TYPE_BOOLEAN = 1;
+ private static final int AMF_TYPE_STRING = 2;
+ private static final int AMF_TYPE_OBJECT = 3;
+ private static final int AMF_TYPE_ECMA_ARRAY = 8;
+ private static final int AMF_TYPE_END_MARKER = 9;
+ private static final int AMF_TYPE_STRICT_ARRAY = 10;
+ private static final int AMF_TYPE_DATE = 11;
+
+ private long durationUs;
+
+ public ScriptTagPayloadReader() {
+ super(new DummyTrackOutput());
+ durationUs = C.TIME_UNSET;
+ }
+
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public void seek() {
+ // Do nothing.
+ }
+
+ @Override
+ protected boolean parseHeader(ParsableByteArray data) {
+ return true;
+ }
+
+ @Override
+ protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
+ int nameType = readAmfType(data);
+ if (nameType != AMF_TYPE_STRING) {
+ // Should never happen.
+ throw new ParserException();
+ }
+ String name = readAmfString(data);
+ if (!NAME_METADATA.equals(name)) {
+ // We're only interested in metadata.
+ return false;
+ }
+ int type = readAmfType(data);
+ if (type != AMF_TYPE_ECMA_ARRAY) {
+ // We're not interested in this metadata.
+ return false;
+ }
+ // Set the duration to the value contained in the metadata, if present.
+ Map<String, Object> metadata = readAmfEcmaArray(data);
+ if (metadata.containsKey(KEY_DURATION)) {
+ double durationSeconds = (double) metadata.get(KEY_DURATION);
+ if (durationSeconds > 0.0) {
+ durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND);
+ }
+ }
+ return false;
+ }
+
+ private static int readAmfType(ParsableByteArray data) {
+ return data.readUnsignedByte();
+ }
+
+ /**
+ * Read a boolean from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static Boolean readAmfBoolean(ParsableByteArray data) {
+ return data.readUnsignedByte() == 1;
+ }
+
+ /**
+ * Read a double number from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static Double readAmfDouble(ParsableByteArray data) {
+ return Double.longBitsToDouble(data.readLong());
+ }
+
+ /**
+ * Read a string from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static String readAmfString(ParsableByteArray data) {
+ int size = data.readUnsignedShort();
+ int position = data.getPosition();
+ data.skipBytes(size);
+ return new String(data.data, position, size);
+ }
+
+ /**
+ * Read an array from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static ArrayList<Object> readAmfStrictArray(ParsableByteArray data) {
+ int count = data.readUnsignedIntToInt();
+ ArrayList<Object> list = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ int type = readAmfType(data);
+ Object value = readAmfData(data, type);
+ if (value != null) {
+ list.add(value);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Read an object from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static HashMap<String, Object> readAmfObject(ParsableByteArray data) {
+ HashMap<String, Object> array = new HashMap<>();
+ while (true) {
+ String key = readAmfString(data);
+ int type = readAmfType(data);
+ if (type == AMF_TYPE_END_MARKER) {
+ break;
+ }
+ Object value = readAmfData(data, type);
+ if (value != null) {
+ array.put(key, value);
+ }
+ }
+ return array;
+ }
+
+ /**
+ * Read an ECMA array from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static HashMap<String, Object> readAmfEcmaArray(ParsableByteArray data) {
+ int count = data.readUnsignedIntToInt();
+ HashMap<String, Object> array = new HashMap<>(count);
+ for (int i = 0; i < count; i++) {
+ String key = readAmfString(data);
+ int type = readAmfType(data);
+ Object value = readAmfData(data, type);
+ if (value != null) {
+ array.put(key, value);
+ }
+ }
+ return array;
+ }
+
+ /**
+ * Read a date from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static Date readAmfDate(ParsableByteArray data) {
+ Date date = new Date((long) readAmfDouble(data).doubleValue());
+ data.skipBytes(2); // Skip reserved bytes.
+ return date;
+ }
+
+ @Nullable
+ private static Object readAmfData(ParsableByteArray data, int type) {
+ switch (type) {
+ case AMF_TYPE_NUMBER:
+ return readAmfDouble(data);
+ case AMF_TYPE_BOOLEAN:
+ return readAmfBoolean(data);
+ case AMF_TYPE_STRING:
+ return readAmfString(data);
+ case AMF_TYPE_OBJECT:
+ return readAmfObject(data);
+ case AMF_TYPE_ECMA_ARRAY:
+ return readAmfEcmaArray(data);
+ case AMF_TYPE_STRICT_ARRAY:
+ return readAmfStrictArray(data);
+ case AMF_TYPE_DATE:
+ return readAmfDate(data);
+ default:
+ // We don't log a warning because there are types that we knowingly don't support.
+ return null;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java
new file mode 100644
index 0000000000..3f8b51244a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java
@@ -0,0 +1,87 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Extracts individual samples from FLV tags, preserving original order.
+ */
+/* package */ abstract class TagPayloadReader {
+
+ /**
+ * Thrown when the format is not supported.
+ */
+ public static final class UnsupportedFormatException extends ParserException {
+
+ public UnsupportedFormatException(String msg) {
+ super(msg);
+ }
+
+ }
+
+ protected final TrackOutput output;
+
+ /**
+ * @param output A {@link TrackOutput} to which samples should be written.
+ */
+ protected TagPayloadReader(TrackOutput output) {
+ this.output = output;
+ }
+
+ /**
+ * Notifies the reader that a seek has occurred.
+ * <p>
+ * Following a call to this method, the data passed to the next invocation of
+ * {@link #consume(ParsableByteArray, long)} will not be a continuation of the data that
+ * was previously passed. Hence the reader should reset any internal state.
+ */
+ public abstract void seek();
+
+ /**
+ * Consumes payload data.
+ *
+ * @param data The payload data to consume.
+ * @param timeUs The timestamp associated with the payload.
+ * @return Whether a sample was output.
+ * @throws ParserException If an error occurs parsing the data.
+ */
+ public final boolean consume(ParsableByteArray data, long timeUs) throws ParserException {
+ return parseHeader(data) && parsePayload(data, timeUs);
+ }
+
+ /**
+ * Parses tag header.
+ *
+ * @param data Buffer where the tag header is stored.
+ * @return Whether the header was parsed successfully.
+ * @throws ParserException If an error occurs parsing the header.
+ */
+ protected abstract boolean parseHeader(ParsableByteArray data) throws ParserException;
+
+ /**
+ * Parses tag payload.
+ *
+ * @param data Buffer where tag payload is stored.
+ * @param timeUs Time position of the frame.
+ * @return Whether a sample was output.
+ * @throws ParserException If an error occurs parsing the payload.
+ */
+ protected abstract boolean parsePayload(ParsableByteArray data, long timeUs)
+ throws ParserException;
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java
new file mode 100644
index 0000000000..6ed5206144
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java
@@ -0,0 +1,141 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig;
+
+/**
+ * Parses video tags from an FLV stream and extracts H.264 nal units.
+ */
+/* package */ final class VideoTagPayloadReader extends TagPayloadReader {
+
+ // Video codec.
+ private static final int VIDEO_CODEC_AVC = 7;
+
+ // Frame types.
+ private static final int VIDEO_FRAME_KEYFRAME = 1;
+ private static final int VIDEO_FRAME_VIDEO_INFO = 5;
+
+ // Packet types.
+ private static final int AVC_PACKET_TYPE_SEQUENCE_HEADER = 0;
+ private static final int AVC_PACKET_TYPE_AVC_NALU = 1;
+
+ // Temporary arrays.
+ private final ParsableByteArray nalStartCode;
+ private final ParsableByteArray nalLength;
+ private int nalUnitLengthFieldLength;
+
+ // State variables.
+ private boolean hasOutputFormat;
+ private boolean hasOutputKeyframe;
+ private int frameType;
+
+ /**
+ * @param output A {@link TrackOutput} to which samples should be written.
+ */
+ public VideoTagPayloadReader(TrackOutput output) {
+ super(output);
+ nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
+ nalLength = new ParsableByteArray(4);
+ }
+
+ @Override
+ public void seek() {
+ hasOutputKeyframe = false;
+ }
+
+ @Override
+ protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException {
+ int header = data.readUnsignedByte();
+ int frameType = (header >> 4) & 0x0F;
+ int videoCodec = (header & 0x0F);
+ // Support just H.264 encoded content.
+ if (videoCodec != VIDEO_CODEC_AVC) {
+ throw new UnsupportedFormatException("Video format not supported: " + videoCodec);
+ }
+ this.frameType = frameType;
+ return (frameType != VIDEO_FRAME_VIDEO_INFO);
+ }
+
+ @Override
+ protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
+ int packetType = data.readUnsignedByte();
+ int compositionTimeMs = data.readInt24();
+
+ timeUs += compositionTimeMs * 1000L;
+ // Parse avc sequence header in case this was not done before.
+ if (packetType == AVC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {
+ ParsableByteArray videoSequence = new ParsableByteArray(new byte[data.bytesLeft()]);
+ data.readBytes(videoSequence.data, 0, data.bytesLeft());
+ AvcConfig avcConfig = AvcConfig.parse(videoSequence);
+ nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;
+ // Construct and output the format.
+ Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null,
+ Format.NO_VALUE, Format.NO_VALUE, avcConfig.width, avcConfig.height, Format.NO_VALUE,
+ avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null);
+ output.format(format);
+ hasOutputFormat = true;
+ return false;
+ } else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) {
+ boolean isKeyframe = frameType == VIDEO_FRAME_KEYFRAME;
+ if (!hasOutputKeyframe && !isKeyframe) {
+ return false;
+ }
+ // TODO: Deduplicate with Mp4Extractor.
+ // 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[] nalLengthData = nalLength.data;
+ nalLengthData[0] = 0;
+ nalLengthData[1] = 0;
+ nalLengthData[2] = 0;
+ int nalUnitLengthFieldLengthDiff = 4 - 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.
+ int bytesWritten = 0;
+ int bytesToWrite;
+ while (data.bytesLeft() > 0) {
+ // Read the NAL length so that we know where we find the next one.
+ data.readBytes(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
+ nalLength.setPosition(0);
+ bytesToWrite = nalLength.readUnsignedIntToInt();
+
+ // Write a start code for the current NAL unit.
+ nalStartCode.setPosition(0);
+ output.sampleData(nalStartCode, 4);
+ bytesWritten += 4;
+
+ // Write the payload of the NAL unit.
+ output.sampleData(data, bytesToWrite);
+ bytesWritten += bytesToWrite;
+ }
+ output.sampleMetadata(
+ timeUs, isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0, bytesWritten, 0, null);
+ hasOutputKeyframe = true;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
new file mode 100644
index 0000000000..b4e160fa74
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
@@ -0,0 +1,260 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv;
+
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayDeque;
+
+/**
+ * Default implementation of {@link EbmlReader}.
+ */
+/* package */ final class DefaultEbmlReader implements EbmlReader {
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ELEMENT_STATE_READ_ID, ELEMENT_STATE_READ_CONTENT_SIZE, ELEMENT_STATE_READ_CONTENT})
+ private @interface ElementState {}
+
+ private static final int ELEMENT_STATE_READ_ID = 0;
+ private static final int ELEMENT_STATE_READ_CONTENT_SIZE = 1;
+ private static final int ELEMENT_STATE_READ_CONTENT = 2;
+
+ private static final int MAX_ID_BYTES = 4;
+ private static final int MAX_LENGTH_BYTES = 8;
+
+ private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8;
+ private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
+ private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
+
+ private final byte[] scratch;
+ private final ArrayDeque<MasterElement> masterElementsStack;
+ private final VarintReader varintReader;
+
+ private EbmlProcessor processor;
+ private @ElementState int elementState;
+ private int elementId;
+ private long elementContentSize;
+
+ public DefaultEbmlReader() {
+ scratch = new byte[8];
+ masterElementsStack = new ArrayDeque<>();
+ varintReader = new VarintReader();
+ }
+
+ @Override
+ public void init(EbmlProcessor processor) {
+ this.processor = processor;
+ }
+
+ @Override
+ public void reset() {
+ elementState = ELEMENT_STATE_READ_ID;
+ masterElementsStack.clear();
+ varintReader.reset();
+ }
+
+ @Override
+ public boolean read(ExtractorInput input) throws IOException, InterruptedException {
+ Assertions.checkNotNull(processor);
+ while (true) {
+ if (!masterElementsStack.isEmpty()
+ && input.getPosition() >= masterElementsStack.peek().elementEndPosition) {
+ processor.endMasterElement(masterElementsStack.pop().elementId);
+ return true;
+ }
+
+ if (elementState == ELEMENT_STATE_READ_ID) {
+ long result = varintReader.readUnsignedVarint(input, true, false, MAX_ID_BYTES);
+ if (result == C.RESULT_MAX_LENGTH_EXCEEDED) {
+ result = maybeResyncToNextLevel1Element(input);
+ }
+ if (result == C.RESULT_END_OF_INPUT) {
+ return false;
+ }
+ // Element IDs are at most 4 bytes, so we can cast to integers.
+ elementId = (int) result;
+ elementState = ELEMENT_STATE_READ_CONTENT_SIZE;
+ }
+
+ if (elementState == ELEMENT_STATE_READ_CONTENT_SIZE) {
+ elementContentSize = varintReader.readUnsignedVarint(input, false, true, MAX_LENGTH_BYTES);
+ elementState = ELEMENT_STATE_READ_CONTENT;
+ }
+
+ @EbmlProcessor.ElementType int type = processor.getElementType(elementId);
+ switch (type) {
+ case EbmlProcessor.ELEMENT_TYPE_MASTER:
+ long elementContentPosition = input.getPosition();
+ long elementEndPosition = elementContentPosition + elementContentSize;
+ masterElementsStack.push(new MasterElement(elementId, elementEndPosition));
+ processor.startMasterElement(elementId, elementContentPosition, elementContentSize);
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT:
+ if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
+ throw new ParserException("Invalid integer size: " + elementContentSize);
+ }
+ processor.integerElement(elementId, readInteger(input, (int) elementContentSize));
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case EbmlProcessor.ELEMENT_TYPE_FLOAT:
+ if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
+ && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
+ throw new ParserException("Invalid float size: " + elementContentSize);
+ }
+ processor.floatElement(elementId, readFloat(input, (int) elementContentSize));
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case EbmlProcessor.ELEMENT_TYPE_STRING:
+ if (elementContentSize > Integer.MAX_VALUE) {
+ throw new ParserException("String element size: " + elementContentSize);
+ }
+ processor.stringElement(elementId, readString(input, (int) elementContentSize));
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case EbmlProcessor.ELEMENT_TYPE_BINARY:
+ processor.binaryElement(elementId, (int) elementContentSize, input);
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case EbmlProcessor.ELEMENT_TYPE_UNKNOWN:
+ input.skipFully((int) elementContentSize);
+ elementState = ELEMENT_STATE_READ_ID;
+ break;
+ default:
+ throw new ParserException("Invalid element type " + type);
+ }
+ }
+ }
+
+ /**
+ * Does a byte by byte search to try and find the next level 1 element. This method is called if
+ * some invalid data is encountered in the parser.
+ *
+ * @param input The {@link ExtractorInput} from which data has to be read.
+ * @return id of the next level 1 element that has been found.
+ * @throws EOFException If the end of input was encountered when searching for the next level 1
+ * element.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private long maybeResyncToNextLevel1Element(ExtractorInput input) throws IOException,
+ InterruptedException {
+ input.resetPeekPosition();
+ while (true) {
+ input.peekFully(scratch, 0, MAX_ID_BYTES);
+ int varintLength = VarintReader.parseUnsignedVarintLength(scratch[0]);
+ if (varintLength != C.LENGTH_UNSET && varintLength <= MAX_ID_BYTES) {
+ int potentialId = (int) VarintReader.assembleVarint(scratch, varintLength, false);
+ if (processor.isLevel1Element(potentialId)) {
+ input.skipFully(varintLength);
+ return potentialId;
+ }
+ }
+ input.skipFully(1);
+ }
+ }
+
+ /**
+ * Reads and returns an integer of length {@code byteLength} from the {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @param byteLength The length of the integer being read.
+ * @return The read integer value.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private long readInteger(ExtractorInput input, int byteLength)
+ throws IOException, InterruptedException {
+ input.readFully(scratch, 0, byteLength);
+ long value = 0;
+ for (int i = 0; i < byteLength; i++) {
+ value = (value << 8) | (scratch[i] & 0xFF);
+ }
+ return value;
+ }
+
+ /**
+ * Reads and returns a float of length {@code byteLength} from the {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @param byteLength The length of the float being read.
+ * @return The read float value.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private double readFloat(ExtractorInput input, int byteLength)
+ throws IOException, InterruptedException {
+ long integerValue = readInteger(input, byteLength);
+ double floatValue;
+ if (byteLength == VALID_FLOAT32_ELEMENT_SIZE_BYTES) {
+ floatValue = Float.intBitsToFloat((int) integerValue);
+ } else {
+ floatValue = Double.longBitsToDouble(integerValue);
+ }
+ return floatValue;
+ }
+
+ /**
+ * Reads a string of length {@code byteLength} from the {@link ExtractorInput}. Zero padding is
+ * removed, so the returned string may be shorter than {@code byteLength}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @param byteLength The length of the string being read, including zero padding.
+ * @return The read string value.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private String readString(ExtractorInput input, int byteLength)
+ throws IOException, InterruptedException {
+ if (byteLength == 0) {
+ return "";
+ }
+ byte[] stringBytes = new byte[byteLength];
+ input.readFully(stringBytes, 0, byteLength);
+ // Remove zero padding.
+ int trimmedLength = byteLength;
+ while (trimmedLength > 0 && stringBytes[trimmedLength - 1] == 0) {
+ trimmedLength--;
+ }
+ return new String(stringBytes, 0, trimmedLength);
+ }
+
+ /**
+ * Used in {@link #masterElementsStack} to track when the current master element ends, so that
+ * {@link EbmlProcessor#endMasterElement(int)} can be called.
+ */
+ private static final class MasterElement {
+
+ private final int elementId;
+ private final long elementEndPosition;
+
+ private MasterElement(int elementId, long elementEndPosition) {
+ this.elementId = elementId;
+ this.elementEndPosition = elementEndPosition;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java
new file mode 100644
index 0000000000..188ced0554
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv;
+
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Defines EBML element IDs/types and processes events. */
+public interface EbmlProcessor {
+
+ /**
+ * EBML element types. One of {@link #ELEMENT_TYPE_UNKNOWN}, {@link #ELEMENT_TYPE_MASTER}, {@link
+ * #ELEMENT_TYPE_UNSIGNED_INT}, {@link #ELEMENT_TYPE_STRING}, {@link #ELEMENT_TYPE_BINARY} or
+ * {@link #ELEMENT_TYPE_FLOAT}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ ELEMENT_TYPE_UNKNOWN,
+ ELEMENT_TYPE_MASTER,
+ ELEMENT_TYPE_UNSIGNED_INT,
+ ELEMENT_TYPE_STRING,
+ ELEMENT_TYPE_BINARY,
+ ELEMENT_TYPE_FLOAT
+ })
+ @interface ElementType {}
+ /** Type for unknown elements. */
+ int ELEMENT_TYPE_UNKNOWN = 0;
+ /** Type for elements that contain child elements. */
+ int ELEMENT_TYPE_MASTER = 1;
+ /** Type for integer value elements of up to 8 bytes. */
+ int ELEMENT_TYPE_UNSIGNED_INT = 2;
+ /** Type for string elements. */
+ int ELEMENT_TYPE_STRING = 3;
+ /** Type for binary elements. */
+ int ELEMENT_TYPE_BINARY = 4;
+ /** Type for IEEE floating point value elements of either 4 or 8 bytes. */
+ int ELEMENT_TYPE_FLOAT = 5;
+
+ /**
+ * Maps an element ID to a corresponding type.
+ *
+ * <p>If {@link #ELEMENT_TYPE_UNKNOWN} is returned then the element is skipped. Note that all
+ * children of a skipped element are also skipped.
+ *
+ * @param id The element ID to map.
+ * @return One of {@link #ELEMENT_TYPE_UNKNOWN}, {@link #ELEMENT_TYPE_MASTER}, {@link
+ * #ELEMENT_TYPE_UNSIGNED_INT}, {@link #ELEMENT_TYPE_STRING}, {@link #ELEMENT_TYPE_BINARY} and
+ * {@link #ELEMENT_TYPE_FLOAT}.
+ */
+ @ElementType
+ int getElementType(int id);
+
+ /**
+ * Checks if the given id is that of a level 1 element.
+ *
+ * @param id The element ID.
+ * @return Whether the given id is that of a level 1 element.
+ */
+ boolean isLevel1Element(int id);
+
+ /**
+ * Called when the start of a master element is encountered.
+ * <p>
+ * Following events should be considered as taking place within this element until a matching call
+ * to {@link #endMasterElement(int)} is made.
+ * <p>
+ * Note that it is possible for another master element of the same element ID to be nested within
+ * itself.
+ *
+ * @param id The element ID.
+ * @param contentPosition The position of the start of the element's content in the stream.
+ * @param contentSize The size of the element's content in bytes.
+ * @throws ParserException If a parsing error occurs.
+ */
+ void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException;
+
+ /**
+ * Called when the end of a master element is encountered.
+ *
+ * @param id The element ID.
+ * @throws ParserException If a parsing error occurs.
+ */
+ void endMasterElement(int id) throws ParserException;
+
+ /**
+ * Called when an integer element is encountered.
+ *
+ * @param id The element ID.
+ * @param value The integer value that the element contains.
+ * @throws ParserException If a parsing error occurs.
+ */
+ void integerElement(int id, long value) throws ParserException;
+
+ /**
+ * Called when a float element is encountered.
+ *
+ * @param id The element ID.
+ * @param value The float value that the element contains
+ * @throws ParserException If a parsing error occurs.
+ */
+ void floatElement(int id, double value) throws ParserException;
+
+ /**
+ * Called when a string element is encountered.
+ *
+ * @param id The element ID.
+ * @param value The string value that the element contains.
+ * @throws ParserException If a parsing error occurs.
+ */
+ void stringElement(int id, String value) throws ParserException;
+
+ /**
+ * Called when a binary element is encountered.
+ * <p>
+ * The element header (containing the element ID and content size) will already have been read.
+ * Implementations are required to consume the whole remainder of the element, which is
+ * {@code contentSize} bytes in length, before returning. Implementations are permitted to fail
+ * (by throwing an exception) having partially consumed the data, however if they do this, they
+ * must consume the remainder of the content when called again.
+ *
+ * @param id The element ID.
+ * @param contentsSize The element's content size.
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @throws ParserException If a parsing error occurs.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void binaryElement(int id, int contentsSize, ExtractorInput input)
+ throws IOException, InterruptedException;
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java
new file mode 100644
index 0000000000..1416a9087e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java
@@ -0,0 +1,57 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import java.io.IOException;
+
+/**
+ * Event-driven EBML reader that delivers events to an {@link EbmlProcessor}.
+ *
+ * <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers. It was
+ * originally designed for the Matroska container format. More information about EBML and Matroska
+ * is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
+ */
+/* package */ interface EbmlReader {
+
+ /**
+ * Initializes the extractor with an {@link EbmlProcessor}.
+ *
+ * @param processor An {@link EbmlProcessor} to process events.
+ */
+ void init(EbmlProcessor processor);
+
+ /**
+ * Resets the state of the reader.
+ * <p>
+ * Subsequent calls to {@link #read(ExtractorInput)} will start reading a new EBML structure
+ * from scratch.
+ */
+ void reset();
+
+ /**
+ * Reads from an {@link ExtractorInput}, invoking an event callback if possible.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @return True if data can continue to be read. False if the end of the input was encountered.
+ * @throws ParserException If parsing fails.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ boolean read(ExtractorInput input) throws IOException, InterruptedException;
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
new file mode 100644
index 0000000000..d9587cd27e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
@@ -0,0 +1,2331 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv;
+
+import android.util.Pair;
+import android.util.SparseArray;
+import androidx.annotation.CallSuper;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ChunkIndex;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.LongArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.video.ColorInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.video.HevcConfig;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+/** Extracts data from the Matroska and WebM container formats. */
+public class MatroskaExtractor implements Extractor {
+
+ /** Factory for {@link MatroskaExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new MatroskaExtractor()};
+
+ /**
+ * Flags controlling the behavior of the extractor. Possible flag value is {@link
+ * #FLAG_DISABLE_SEEK_FOR_CUES}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {FLAG_DISABLE_SEEK_FOR_CUES})
+ public @interface Flags {}
+ /**
+ * Flag to disable seeking for cues.
+ * <p>
+ * Normally (i.e. when this flag is not set) the extractor will seek to the cues element if its
+ * position is specified in the seek head and if it's after the first cluster. Setting this flag
+ * disables seeking to the cues element. If the cues element is after the first cluster then the
+ * media is treated as being unseekable.
+ */
+ public static final int FLAG_DISABLE_SEEK_FOR_CUES = 1;
+
+ private static final String TAG = "MatroskaExtractor";
+
+ private static final int UNSET_ENTRY_ID = -1;
+
+ private static final int BLOCK_STATE_START = 0;
+ private static final int BLOCK_STATE_HEADER = 1;
+ private static final int BLOCK_STATE_DATA = 2;
+
+ private static final String DOC_TYPE_MATROSKA = "matroska";
+ private static final String DOC_TYPE_WEBM = "webm";
+ private static final String CODEC_ID_VP8 = "V_VP8";
+ private static final String CODEC_ID_VP9 = "V_VP9";
+ private static final String CODEC_ID_AV1 = "V_AV1";
+ private static final String CODEC_ID_MPEG2 = "V_MPEG2";
+ private static final String CODEC_ID_MPEG4_SP = "V_MPEG4/ISO/SP";
+ private static final String CODEC_ID_MPEG4_ASP = "V_MPEG4/ISO/ASP";
+ private static final String CODEC_ID_MPEG4_AP = "V_MPEG4/ISO/AP";
+ private static final String CODEC_ID_H264 = "V_MPEG4/ISO/AVC";
+ private static final String CODEC_ID_H265 = "V_MPEGH/ISO/HEVC";
+ private static final String CODEC_ID_FOURCC = "V_MS/VFW/FOURCC";
+ private static final String CODEC_ID_THEORA = "V_THEORA";
+ private static final String CODEC_ID_VORBIS = "A_VORBIS";
+ private static final String CODEC_ID_OPUS = "A_OPUS";
+ private static final String CODEC_ID_AAC = "A_AAC";
+ private static final String CODEC_ID_MP2 = "A_MPEG/L2";
+ private static final String CODEC_ID_MP3 = "A_MPEG/L3";
+ private static final String CODEC_ID_AC3 = "A_AC3";
+ private static final String CODEC_ID_E_AC3 = "A_EAC3";
+ private static final String CODEC_ID_TRUEHD = "A_TRUEHD";
+ private static final String CODEC_ID_DTS = "A_DTS";
+ private static final String CODEC_ID_DTS_EXPRESS = "A_DTS/EXPRESS";
+ private static final String CODEC_ID_DTS_LOSSLESS = "A_DTS/LOSSLESS";
+ private static final String CODEC_ID_FLAC = "A_FLAC";
+ private static final String CODEC_ID_ACM = "A_MS/ACM";
+ private static final String CODEC_ID_PCM_INT_LIT = "A_PCM/INT/LIT";
+ private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8";
+ private static final String CODEC_ID_ASS = "S_TEXT/ASS";
+ private static final String CODEC_ID_VOBSUB = "S_VOBSUB";
+ private static final String CODEC_ID_PGS = "S_HDMV/PGS";
+ private static final String CODEC_ID_DVBSUB = "S_DVBSUB";
+
+ private static final int VORBIS_MAX_INPUT_SIZE = 8192;
+ private static final int OPUS_MAX_INPUT_SIZE = 5760;
+ private static final int ENCRYPTION_IV_SIZE = 8;
+ private static final int TRACK_TYPE_AUDIO = 2;
+
+ private static final int ID_EBML = 0x1A45DFA3;
+ private static final int ID_EBML_READ_VERSION = 0x42F7;
+ private static final int ID_DOC_TYPE = 0x4282;
+ private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
+ private static final int ID_SEGMENT = 0x18538067;
+ private static final int ID_SEGMENT_INFO = 0x1549A966;
+ private static final int ID_SEEK_HEAD = 0x114D9B74;
+ private static final int ID_SEEK = 0x4DBB;
+ private static final int ID_SEEK_ID = 0x53AB;
+ private static final int ID_SEEK_POSITION = 0x53AC;
+ private static final int ID_INFO = 0x1549A966;
+ private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
+ private static final int ID_DURATION = 0x4489;
+ private static final int ID_CLUSTER = 0x1F43B675;
+ private static final int ID_TIME_CODE = 0xE7;
+ private static final int ID_SIMPLE_BLOCK = 0xA3;
+ private static final int ID_BLOCK_GROUP = 0xA0;
+ private static final int ID_BLOCK = 0xA1;
+ private static final int ID_BLOCK_DURATION = 0x9B;
+ private static final int ID_BLOCK_ADDITIONS = 0x75A1;
+ private static final int ID_BLOCK_MORE = 0xA6;
+ private static final int ID_BLOCK_ADD_ID = 0xEE;
+ private static final int ID_BLOCK_ADDITIONAL = 0xA5;
+ private static final int ID_REFERENCE_BLOCK = 0xFB;
+ private static final int ID_TRACKS = 0x1654AE6B;
+ private static final int ID_TRACK_ENTRY = 0xAE;
+ private static final int ID_TRACK_NUMBER = 0xD7;
+ private static final int ID_TRACK_TYPE = 0x83;
+ private static final int ID_FLAG_DEFAULT = 0x88;
+ private static final int ID_FLAG_FORCED = 0x55AA;
+ private static final int ID_DEFAULT_DURATION = 0x23E383;
+ private static final int ID_MAX_BLOCK_ADDITION_ID = 0x55EE;
+ private static final int ID_NAME = 0x536E;
+ private static final int ID_CODEC_ID = 0x86;
+ private static final int ID_CODEC_PRIVATE = 0x63A2;
+ private static final int ID_CODEC_DELAY = 0x56AA;
+ private static final int ID_SEEK_PRE_ROLL = 0x56BB;
+ private static final int ID_VIDEO = 0xE0;
+ private static final int ID_PIXEL_WIDTH = 0xB0;
+ private static final int ID_PIXEL_HEIGHT = 0xBA;
+ private static final int ID_DISPLAY_WIDTH = 0x54B0;
+ private static final int ID_DISPLAY_HEIGHT = 0x54BA;
+ private static final int ID_DISPLAY_UNIT = 0x54B2;
+ private static final int ID_AUDIO = 0xE1;
+ private static final int ID_CHANNELS = 0x9F;
+ private static final int ID_AUDIO_BIT_DEPTH = 0x6264;
+ private static final int ID_SAMPLING_FREQUENCY = 0xB5;
+ private static final int ID_CONTENT_ENCODINGS = 0x6D80;
+ private static final int ID_CONTENT_ENCODING = 0x6240;
+ private static final int ID_CONTENT_ENCODING_ORDER = 0x5031;
+ private static final int ID_CONTENT_ENCODING_SCOPE = 0x5032;
+ private static final int ID_CONTENT_COMPRESSION = 0x5034;
+ private static final int ID_CONTENT_COMPRESSION_ALGORITHM = 0x4254;
+ private static final int ID_CONTENT_COMPRESSION_SETTINGS = 0x4255;
+ private static final int ID_CONTENT_ENCRYPTION = 0x5035;
+ private static final int ID_CONTENT_ENCRYPTION_ALGORITHM = 0x47E1;
+ private static final int ID_CONTENT_ENCRYPTION_KEY_ID = 0x47E2;
+ private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS = 0x47E7;
+ private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE = 0x47E8;
+ private static final int ID_CUES = 0x1C53BB6B;
+ private static final int ID_CUE_POINT = 0xBB;
+ private static final int ID_CUE_TIME = 0xB3;
+ private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
+ private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
+ private static final int ID_LANGUAGE = 0x22B59C;
+ private static final int ID_PROJECTION = 0x7670;
+ private static final int ID_PROJECTION_TYPE = 0x7671;
+ private static final int ID_PROJECTION_PRIVATE = 0x7672;
+ private static final int ID_PROJECTION_POSE_YAW = 0x7673;
+ private static final int ID_PROJECTION_POSE_PITCH = 0x7674;
+ private static final int ID_PROJECTION_POSE_ROLL = 0x7675;
+ private static final int ID_STEREO_MODE = 0x53B8;
+ private static final int ID_COLOUR = 0x55B0;
+ private static final int ID_COLOUR_RANGE = 0x55B9;
+ private static final int ID_COLOUR_TRANSFER = 0x55BA;
+ private static final int ID_COLOUR_PRIMARIES = 0x55BB;
+ private static final int ID_MAX_CLL = 0x55BC;
+ private static final int ID_MAX_FALL = 0x55BD;
+ private static final int ID_MASTERING_METADATA = 0x55D0;
+ private static final int ID_PRIMARY_R_CHROMATICITY_X = 0x55D1;
+ private static final int ID_PRIMARY_R_CHROMATICITY_Y = 0x55D2;
+ private static final int ID_PRIMARY_G_CHROMATICITY_X = 0x55D3;
+ private static final int ID_PRIMARY_G_CHROMATICITY_Y = 0x55D4;
+ private static final int ID_PRIMARY_B_CHROMATICITY_X = 0x55D5;
+ private static final int ID_PRIMARY_B_CHROMATICITY_Y = 0x55D6;
+ private static final int ID_WHITE_POINT_CHROMATICITY_X = 0x55D7;
+ private static final int ID_WHITE_POINT_CHROMATICITY_Y = 0x55D8;
+ private static final int ID_LUMNINANCE_MAX = 0x55D9;
+ private static final int ID_LUMNINANCE_MIN = 0x55DA;
+
+ /**
+ * BlockAddID value for ITU T.35 metadata in a VP9 track. See also
+ * https://www.webmproject.org/docs/container/.
+ */
+ private static final int BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 = 4;
+
+ private static final int LACING_NONE = 0;
+ private static final int LACING_XIPH = 1;
+ private static final int LACING_FIXED_SIZE = 2;
+ private static final int LACING_EBML = 3;
+
+ private static final int FOURCC_COMPRESSION_DIVX = 0x58564944;
+ private static final int FOURCC_COMPRESSION_H263 = 0x33363248;
+ private static final int FOURCC_COMPRESSION_VC1 = 0x31435657;
+
+ /**
+ * A template for the prefix that must be added to each subrip sample.
+ *
+ * <p>The display time of each subtitle is passed as {@code timeUs} to {@link
+ * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to
+ * {@code timeUs}. Hence the start timecode is always zero. The 12 byte end timecode starting at
+ * {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be replaced with
+ * the duration of the subtitle.
+ *
+ * <p>Equivalent to the UTF-8 string: "1\n00:00:00,000 --> 00:00:00,000\n".
+ */
+ private static final byte[] SUBRIP_PREFIX =
+ new byte[] {
+ 49, 10, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48,
+ 48, 58, 48, 48, 44, 48, 48, 48, 10
+ };
+ /**
+ * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}.
+ */
+ private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19;
+ /**
+ * The value by which to divide a time in microseconds to convert it to the unit of the last value
+ * in a subrip timecode (milliseconds).
+ */
+ private static final long SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR = 1000;
+ /**
+ * The format of a subrip timecode.
+ */
+ private static final String SUBRIP_TIMECODE_FORMAT = "%02d:%02d:%02d,%03d";
+
+ /**
+ * Matroska specific format line for SSA subtitles.
+ */
+ private static final byte[] SSA_DIALOGUE_FORMAT = Util.getUtf8Bytes("Format: Start, End, "
+ + "ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
+ /**
+ * A template for the prefix that must be added to each SSA sample.
+ *
+ * <p>The display time of each subtitle is passed as {@code timeUs} to {@link
+ * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to
+ * {@code timeUs}. Hence the start timecode is always zero. The 12 byte end timecode starting at
+ * {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be replaced with
+ * the duration of the subtitle.
+ *
+ * <p>Equivalent to the UTF-8 string: "Dialogue: 0:00:00:00,0:00:00:00,".
+ */
+ private static final byte[] SSA_PREFIX =
+ new byte[] {
+ 68, 105, 97, 108, 111, 103, 117, 101, 58, 32, 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44,
+ 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44
+ };
+ /**
+ * The byte offset of the end timecode in {@link #SSA_PREFIX}.
+ */
+ private static final int SSA_PREFIX_END_TIMECODE_OFFSET = 21;
+ /**
+ * The value by which to divide a time in microseconds to convert it to the unit of the last value
+ * in an SSA timecode (1/100ths of a second).
+ */
+ private static final long SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR = 10000;
+ /**
+ * The format of an SSA timecode.
+ */
+ private static final String SSA_TIMECODE_FORMAT = "%01d:%02d:%02d:%02d";
+
+ /**
+ * The length in bytes of a WAVEFORMATEX structure.
+ */
+ private static final int WAVE_FORMAT_SIZE = 18;
+ /**
+ * Format tag indicating a WAVEFORMATEXTENSIBLE structure.
+ */
+ private static final int WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
+ /**
+ * Format tag for PCM.
+ */
+ private static final int WAVE_FORMAT_PCM = 1;
+ /**
+ * Sub format for PCM.
+ */
+ private static final UUID WAVE_SUBFORMAT_PCM = new UUID(0x0100000000001000L, 0x800000AA00389B71L);
+
+ private final EbmlReader reader;
+ private final VarintReader varintReader;
+ private final SparseArray<Track> tracks;
+ private final boolean seekForCuesEnabled;
+
+ // Temporary arrays.
+ private final ParsableByteArray nalStartCode;
+ private final ParsableByteArray nalLength;
+ private final ParsableByteArray scratch;
+ private final ParsableByteArray vorbisNumPageSamples;
+ private final ParsableByteArray seekEntryIdBytes;
+ private final ParsableByteArray sampleStrippedBytes;
+ private final ParsableByteArray subtitleSample;
+ private final ParsableByteArray encryptionInitializationVector;
+ private final ParsableByteArray encryptionSubsampleData;
+ private final ParsableByteArray blockAdditionalData;
+ private ByteBuffer encryptionSubsampleDataBuffer;
+
+ private long segmentContentSize;
+ private long segmentContentPosition = C.POSITION_UNSET;
+ private long timecodeScale = C.TIME_UNSET;
+ private long durationTimecode = C.TIME_UNSET;
+ private long durationUs = C.TIME_UNSET;
+
+ // The track corresponding to the current TrackEntry element, or null.
+ private Track currentTrack;
+
+ // Whether a seek map has been sent to the output.
+ private boolean sentSeekMap;
+
+ // Master seek entry related elements.
+ private int seekEntryId;
+ private long seekEntryPosition;
+
+ // Cue related elements.
+ private boolean seekForCues;
+ private long cuesContentPosition = C.POSITION_UNSET;
+ private long seekPositionAfterBuildingCues = C.POSITION_UNSET;
+ private long clusterTimecodeUs = C.TIME_UNSET;
+ private LongArray cueTimesUs;
+ private LongArray cueClusterPositions;
+ private boolean seenClusterPositionForCurrentCuePoint;
+
+ // Reading state.
+ private boolean haveOutputSample;
+
+ // Block reading state.
+ private int blockState;
+ private long blockTimeUs;
+ private long blockDurationUs;
+ private int blockSampleIndex;
+ private int blockSampleCount;
+ private int[] blockSampleSizes;
+ private int blockTrackNumber;
+ private int blockTrackNumberLength;
+ @C.BufferFlags
+ private int blockFlags;
+ private int blockAdditionalId;
+ private boolean blockHasReferenceBlock;
+
+ // Sample writing state.
+ private int sampleBytesRead;
+ private int sampleBytesWritten;
+ private int sampleCurrentNalBytesRemaining;
+ private boolean sampleEncodingHandled;
+ private boolean sampleSignalByteRead;
+ private boolean samplePartitionCountRead;
+ private int samplePartitionCount;
+ private byte sampleSignalByte;
+ private boolean sampleInitializationVectorRead;
+
+ // Extractor outputs.
+ private ExtractorOutput extractorOutput;
+
+ public MatroskaExtractor() {
+ this(0);
+ }
+
+ public MatroskaExtractor(@Flags int flags) {
+ this(new DefaultEbmlReader(), flags);
+ }
+
+ /* package */ MatroskaExtractor(EbmlReader reader, @Flags int flags) {
+ this.reader = reader;
+ this.reader.init(new InnerEbmlProcessor());
+ seekForCuesEnabled = (flags & FLAG_DISABLE_SEEK_FOR_CUES) == 0;
+ varintReader = new VarintReader();
+ tracks = new SparseArray<>();
+ scratch = new ParsableByteArray(4);
+ vorbisNumPageSamples = new ParsableByteArray(ByteBuffer.allocate(4).putInt(-1).array());
+ seekEntryIdBytes = new ParsableByteArray(4);
+ nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
+ nalLength = new ParsableByteArray(4);
+ sampleStrippedBytes = new ParsableByteArray();
+ subtitleSample = new ParsableByteArray();
+ encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE);
+ encryptionSubsampleData = new ParsableByteArray();
+ blockAdditionalData = new ParsableByteArray();
+ }
+
+ @Override
+ public final boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return new Sniffer().sniff(input);
+ }
+
+ @Override
+ public final void init(ExtractorOutput output) {
+ extractorOutput = output;
+ }
+
+ @CallSuper
+ @Override
+ public void seek(long position, long timeUs) {
+ clusterTimecodeUs = C.TIME_UNSET;
+ blockState = BLOCK_STATE_START;
+ reader.reset();
+ varintReader.reset();
+ resetWriteSampleData();
+ for (int i = 0; i < tracks.size(); i++) {
+ tracks.valueAt(i).reset();
+ }
+ }
+
+ @Override
+ public final void release() {
+ // Do nothing
+ }
+
+ @Override
+ public final int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ haveOutputSample = false;
+ boolean continueReading = true;
+ while (continueReading && !haveOutputSample) {
+ continueReading = reader.read(input);
+ if (continueReading && maybeSeekForCues(seekPosition, input.getPosition())) {
+ return Extractor.RESULT_SEEK;
+ }
+ }
+ if (!continueReading) {
+ for (int i = 0; i < tracks.size(); i++) {
+ tracks.valueAt(i).outputPendingSampleMetadata();
+ }
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ /**
+ * Maps an element ID to a corresponding type.
+ *
+ * @see EbmlProcessor#getElementType(int)
+ */
+ @CallSuper
+ @EbmlProcessor.ElementType
+ protected int getElementType(int id) {
+ switch (id) {
+ case ID_EBML:
+ case ID_SEGMENT:
+ case ID_SEEK_HEAD:
+ case ID_SEEK:
+ case ID_INFO:
+ case ID_CLUSTER:
+ case ID_TRACKS:
+ case ID_TRACK_ENTRY:
+ case ID_AUDIO:
+ case ID_VIDEO:
+ case ID_CONTENT_ENCODINGS:
+ case ID_CONTENT_ENCODING:
+ case ID_CONTENT_COMPRESSION:
+ case ID_CONTENT_ENCRYPTION:
+ case ID_CONTENT_ENCRYPTION_AES_SETTINGS:
+ case ID_CUES:
+ case ID_CUE_POINT:
+ case ID_CUE_TRACK_POSITIONS:
+ case ID_BLOCK_GROUP:
+ case ID_BLOCK_ADDITIONS:
+ case ID_BLOCK_MORE:
+ case ID_PROJECTION:
+ case ID_COLOUR:
+ case ID_MASTERING_METADATA:
+ return EbmlProcessor.ELEMENT_TYPE_MASTER;
+ case ID_EBML_READ_VERSION:
+ case ID_DOC_TYPE_READ_VERSION:
+ case ID_SEEK_POSITION:
+ case ID_TIMECODE_SCALE:
+ case ID_TIME_CODE:
+ case ID_BLOCK_DURATION:
+ case ID_PIXEL_WIDTH:
+ case ID_PIXEL_HEIGHT:
+ case ID_DISPLAY_WIDTH:
+ case ID_DISPLAY_HEIGHT:
+ case ID_DISPLAY_UNIT:
+ case ID_TRACK_NUMBER:
+ case ID_TRACK_TYPE:
+ case ID_FLAG_DEFAULT:
+ case ID_FLAG_FORCED:
+ case ID_DEFAULT_DURATION:
+ case ID_MAX_BLOCK_ADDITION_ID:
+ case ID_CODEC_DELAY:
+ case ID_SEEK_PRE_ROLL:
+ case ID_CHANNELS:
+ case ID_AUDIO_BIT_DEPTH:
+ case ID_CONTENT_ENCODING_ORDER:
+ case ID_CONTENT_ENCODING_SCOPE:
+ case ID_CONTENT_COMPRESSION_ALGORITHM:
+ case ID_CONTENT_ENCRYPTION_ALGORITHM:
+ case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
+ case ID_CUE_TIME:
+ case ID_CUE_CLUSTER_POSITION:
+ case ID_REFERENCE_BLOCK:
+ case ID_STEREO_MODE:
+ case ID_COLOUR_RANGE:
+ case ID_COLOUR_TRANSFER:
+ case ID_COLOUR_PRIMARIES:
+ case ID_MAX_CLL:
+ case ID_MAX_FALL:
+ case ID_PROJECTION_TYPE:
+ case ID_BLOCK_ADD_ID:
+ return EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT;
+ case ID_DOC_TYPE:
+ case ID_NAME:
+ case ID_CODEC_ID:
+ case ID_LANGUAGE:
+ return EbmlProcessor.ELEMENT_TYPE_STRING;
+ case ID_SEEK_ID:
+ case ID_CONTENT_COMPRESSION_SETTINGS:
+ case ID_CONTENT_ENCRYPTION_KEY_ID:
+ case ID_SIMPLE_BLOCK:
+ case ID_BLOCK:
+ case ID_CODEC_PRIVATE:
+ case ID_PROJECTION_PRIVATE:
+ case ID_BLOCK_ADDITIONAL:
+ return EbmlProcessor.ELEMENT_TYPE_BINARY;
+ case ID_DURATION:
+ case ID_SAMPLING_FREQUENCY:
+ case ID_PRIMARY_R_CHROMATICITY_X:
+ case ID_PRIMARY_R_CHROMATICITY_Y:
+ case ID_PRIMARY_G_CHROMATICITY_X:
+ case ID_PRIMARY_G_CHROMATICITY_Y:
+ case ID_PRIMARY_B_CHROMATICITY_X:
+ case ID_PRIMARY_B_CHROMATICITY_Y:
+ case ID_WHITE_POINT_CHROMATICITY_X:
+ case ID_WHITE_POINT_CHROMATICITY_Y:
+ case ID_LUMNINANCE_MAX:
+ case ID_LUMNINANCE_MIN:
+ case ID_PROJECTION_POSE_YAW:
+ case ID_PROJECTION_POSE_PITCH:
+ case ID_PROJECTION_POSE_ROLL:
+ return EbmlProcessor.ELEMENT_TYPE_FLOAT;
+ default:
+ return EbmlProcessor.ELEMENT_TYPE_UNKNOWN;
+ }
+ }
+
+ /**
+ * Checks if the given id is that of a level 1 element.
+ *
+ * @see EbmlProcessor#isLevel1Element(int)
+ */
+ @CallSuper
+ protected boolean isLevel1Element(int id) {
+ return id == ID_SEGMENT_INFO || id == ID_CLUSTER || id == ID_CUES || id == ID_TRACKS;
+ }
+
+ /**
+ * Called when the start of a master element is encountered.
+ *
+ * @see EbmlProcessor#startMasterElement(int, long, long)
+ */
+ @CallSuper
+ protected void startMasterElement(int id, long contentPosition, long contentSize)
+ throws ParserException {
+ switch (id) {
+ case ID_SEGMENT:
+ if (segmentContentPosition != C.POSITION_UNSET
+ && segmentContentPosition != contentPosition) {
+ throw new ParserException("Multiple Segment elements not supported");
+ }
+ segmentContentPosition = contentPosition;
+ segmentContentSize = contentSize;
+ break;
+ case ID_SEEK:
+ seekEntryId = UNSET_ENTRY_ID;
+ seekEntryPosition = C.POSITION_UNSET;
+ break;
+ case ID_CUES:
+ cueTimesUs = new LongArray();
+ cueClusterPositions = new LongArray();
+ break;
+ case ID_CUE_POINT:
+ seenClusterPositionForCurrentCuePoint = false;
+ break;
+ case ID_CLUSTER:
+ if (!sentSeekMap) {
+ // We need to build cues before parsing the cluster.
+ if (seekForCuesEnabled && cuesContentPosition != C.POSITION_UNSET) {
+ // We know where the Cues element is located. Seek to request it.
+ seekForCues = true;
+ } else {
+ // We don't know where the Cues element is located. It's most likely omitted. Allow
+ // playback, but disable seeking.
+ extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
+ sentSeekMap = true;
+ }
+ }
+ break;
+ case ID_BLOCK_GROUP:
+ blockHasReferenceBlock = false;
+ break;
+ case ID_CONTENT_ENCODING:
+ // TODO: check and fail if more than one content encoding is present.
+ break;
+ case ID_CONTENT_ENCRYPTION:
+ currentTrack.hasContentEncryption = true;
+ break;
+ case ID_TRACK_ENTRY:
+ currentTrack = new Track();
+ break;
+ case ID_MASTERING_METADATA:
+ currentTrack.hasColorInfo = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Called when the end of a master element is encountered.
+ *
+ * @see EbmlProcessor#endMasterElement(int)
+ */
+ @CallSuper
+ protected void endMasterElement(int id) throws ParserException {
+ switch (id) {
+ case ID_SEGMENT_INFO:
+ if (timecodeScale == C.TIME_UNSET) {
+ // timecodeScale was omitted. Use the default value.
+ timecodeScale = 1000000;
+ }
+ if (durationTimecode != C.TIME_UNSET) {
+ durationUs = scaleTimecodeToUs(durationTimecode);
+ }
+ break;
+ case ID_SEEK:
+ if (seekEntryId == UNSET_ENTRY_ID || seekEntryPosition == C.POSITION_UNSET) {
+ throw new ParserException("Mandatory element SeekID or SeekPosition not found");
+ }
+ if (seekEntryId == ID_CUES) {
+ cuesContentPosition = seekEntryPosition;
+ }
+ break;
+ case ID_CUES:
+ if (!sentSeekMap) {
+ extractorOutput.seekMap(buildSeekMap());
+ sentSeekMap = true;
+ } else {
+ // We have already built the cues. Ignore.
+ }
+ break;
+ case ID_BLOCK_GROUP:
+ if (blockState != BLOCK_STATE_DATA) {
+ // We've skipped this block (due to incompatible track number).
+ return;
+ }
+ // Commit sample metadata.
+ int sampleOffset = 0;
+ for (int i = 0; i < blockSampleCount; i++) {
+ sampleOffset += blockSampleSizes[i];
+ }
+ Track track = tracks.get(blockTrackNumber);
+ for (int i = 0; i < blockSampleCount; i++) {
+ long sampleTimeUs = blockTimeUs + (i * track.defaultSampleDurationNs) / 1000;
+ int sampleFlags = blockFlags;
+ if (i == 0 && !blockHasReferenceBlock) {
+ // If the ReferenceBlock element was not found in this block, then the first frame is a
+ // keyframe.
+ sampleFlags |= C.BUFFER_FLAG_KEY_FRAME;
+ }
+ int sampleSize = blockSampleSizes[i];
+ sampleOffset -= sampleSize; // The offset is to the end of the sample.
+ commitSampleToOutput(track, sampleTimeUs, sampleFlags, sampleSize, sampleOffset);
+ }
+ blockState = BLOCK_STATE_START;
+ break;
+ case ID_CONTENT_ENCODING:
+ if (currentTrack.hasContentEncryption) {
+ if (currentTrack.cryptoData == null) {
+ throw new ParserException("Encrypted Track found but ContentEncKeyID was not found");
+ }
+ currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL,
+ MimeTypes.VIDEO_WEBM, currentTrack.cryptoData.encryptionKey));
+ }
+ break;
+ case ID_CONTENT_ENCODINGS:
+ if (currentTrack.hasContentEncryption && currentTrack.sampleStrippedBytes != null) {
+ throw new ParserException("Combining encryption and compression is not supported");
+ }
+ break;
+ case ID_TRACK_ENTRY:
+ if (isCodecSupported(currentTrack.codecId)) {
+ currentTrack.initializeOutput(extractorOutput, currentTrack.number);
+ tracks.put(currentTrack.number, currentTrack);
+ }
+ currentTrack = null;
+ break;
+ case ID_TRACKS:
+ if (tracks.size() == 0) {
+ throw new ParserException("No valid tracks were found");
+ }
+ extractorOutput.endTracks();
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Called when an integer element is encountered.
+ *
+ * @see EbmlProcessor#integerElement(int, long)
+ */
+ @CallSuper
+ protected void integerElement(int id, long value) throws ParserException {
+ switch (id) {
+ case ID_EBML_READ_VERSION:
+ // Validate that EBMLReadVersion is supported. This extractor only supports v1.
+ if (value != 1) {
+ throw new ParserException("EBMLReadVersion " + value + " not supported");
+ }
+ break;
+ case ID_DOC_TYPE_READ_VERSION:
+ // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
+ if (value < 1 || value > 2) {
+ throw new ParserException("DocTypeReadVersion " + value + " not supported");
+ }
+ break;
+ case ID_SEEK_POSITION:
+ // Seek Position is the relative offset beginning from the Segment. So to get absolute
+ // offset from the beginning of the file, we need to add segmentContentPosition to it.
+ seekEntryPosition = value + segmentContentPosition;
+ break;
+ case ID_TIMECODE_SCALE:
+ timecodeScale = value;
+ break;
+ case ID_PIXEL_WIDTH:
+ currentTrack.width = (int) value;
+ break;
+ case ID_PIXEL_HEIGHT:
+ currentTrack.height = (int) value;
+ break;
+ case ID_DISPLAY_WIDTH:
+ currentTrack.displayWidth = (int) value;
+ break;
+ case ID_DISPLAY_HEIGHT:
+ currentTrack.displayHeight = (int) value;
+ break;
+ case ID_DISPLAY_UNIT:
+ currentTrack.displayUnit = (int) value;
+ break;
+ case ID_TRACK_NUMBER:
+ currentTrack.number = (int) value;
+ break;
+ case ID_FLAG_DEFAULT:
+ currentTrack.flagDefault = value == 1;
+ break;
+ case ID_FLAG_FORCED:
+ currentTrack.flagForced = value == 1;
+ break;
+ case ID_TRACK_TYPE:
+ currentTrack.type = (int) value;
+ break;
+ case ID_DEFAULT_DURATION:
+ currentTrack.defaultSampleDurationNs = (int) value;
+ break;
+ case ID_MAX_BLOCK_ADDITION_ID:
+ currentTrack.maxBlockAdditionId = (int) value;
+ break;
+ case ID_CODEC_DELAY:
+ currentTrack.codecDelayNs = value;
+ break;
+ case ID_SEEK_PRE_ROLL:
+ currentTrack.seekPreRollNs = value;
+ break;
+ case ID_CHANNELS:
+ currentTrack.channelCount = (int) value;
+ break;
+ case ID_AUDIO_BIT_DEPTH:
+ currentTrack.audioBitDepth = (int) value;
+ break;
+ case ID_REFERENCE_BLOCK:
+ blockHasReferenceBlock = true;
+ break;
+ case ID_CONTENT_ENCODING_ORDER:
+ // This extractor only supports one ContentEncoding element and hence the order has to be 0.
+ if (value != 0) {
+ throw new ParserException("ContentEncodingOrder " + value + " not supported");
+ }
+ break;
+ case ID_CONTENT_ENCODING_SCOPE:
+ // This extractor only supports the scope of all frames.
+ if (value != 1) {
+ throw new ParserException("ContentEncodingScope " + value + " not supported");
+ }
+ break;
+ case ID_CONTENT_COMPRESSION_ALGORITHM:
+ // This extractor only supports header stripping.
+ if (value != 3) {
+ throw new ParserException("ContentCompAlgo " + value + " not supported");
+ }
+ break;
+ case ID_CONTENT_ENCRYPTION_ALGORITHM:
+ // Only the value 5 (AES) is allowed according to the WebM specification.
+ if (value != 5) {
+ throw new ParserException("ContentEncAlgo " + value + " not supported");
+ }
+ break;
+ case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
+ // Only the value 1 is allowed according to the WebM specification.
+ if (value != 1) {
+ throw new ParserException("AESSettingsCipherMode " + value + " not supported");
+ }
+ break;
+ case ID_CUE_TIME:
+ cueTimesUs.add(scaleTimecodeToUs(value));
+ break;
+ case ID_CUE_CLUSTER_POSITION:
+ if (!seenClusterPositionForCurrentCuePoint) {
+ // If there's more than one video/audio track, then there could be more than one
+ // CueTrackPositions within a single CuePoint. In such a case, ignore all but the first
+ // one (since the cluster position will be quite close for all the tracks).
+ cueClusterPositions.add(value);
+ seenClusterPositionForCurrentCuePoint = true;
+ }
+ break;
+ case ID_TIME_CODE:
+ clusterTimecodeUs = scaleTimecodeToUs(value);
+ break;
+ case ID_BLOCK_DURATION:
+ blockDurationUs = scaleTimecodeToUs(value);
+ break;
+ case ID_STEREO_MODE:
+ int layout = (int) value;
+ switch (layout) {
+ case 0:
+ currentTrack.stereoMode = C.STEREO_MODE_MONO;
+ break;
+ case 1:
+ currentTrack.stereoMode = C.STEREO_MODE_LEFT_RIGHT;
+ break;
+ case 3:
+ currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM;
+ break;
+ case 15:
+ currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_COLOUR_PRIMARIES:
+ currentTrack.hasColorInfo = true;
+ switch ((int) value) {
+ case 1:
+ currentTrack.colorSpace = C.COLOR_SPACE_BT709;
+ break;
+ case 4: // BT.470M.
+ case 5: // BT.470BG.
+ case 6: // SMPTE 170M.
+ case 7: // SMPTE 240M.
+ currentTrack.colorSpace = C.COLOR_SPACE_BT601;
+ break;
+ case 9:
+ currentTrack.colorSpace = C.COLOR_SPACE_BT2020;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_COLOUR_TRANSFER:
+ switch ((int) value) {
+ case 1: // BT.709.
+ case 6: // SMPTE 170M.
+ case 7: // SMPTE 240M.
+ currentTrack.colorTransfer = C.COLOR_TRANSFER_SDR;
+ break;
+ case 16:
+ currentTrack.colorTransfer = C.COLOR_TRANSFER_ST2084;
+ break;
+ case 18:
+ currentTrack.colorTransfer = C.COLOR_TRANSFER_HLG;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_COLOUR_RANGE:
+ switch((int) value) {
+ case 1: // Broadcast range.
+ currentTrack.colorRange = C.COLOR_RANGE_LIMITED;
+ break;
+ case 2:
+ currentTrack.colorRange = C.COLOR_RANGE_FULL;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_MAX_CLL:
+ currentTrack.maxContentLuminance = (int) value;
+ break;
+ case ID_MAX_FALL:
+ currentTrack.maxFrameAverageLuminance = (int) value;
+ break;
+ case ID_PROJECTION_TYPE:
+ switch ((int) value) {
+ case 0:
+ currentTrack.projectionType = C.PROJECTION_RECTANGULAR;
+ break;
+ case 1:
+ currentTrack.projectionType = C.PROJECTION_EQUIRECTANGULAR;
+ break;
+ case 2:
+ currentTrack.projectionType = C.PROJECTION_CUBEMAP;
+ break;
+ case 3:
+ currentTrack.projectionType = C.PROJECTION_MESH;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_BLOCK_ADD_ID:
+ blockAdditionalId = (int) value;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Called when a float element is encountered.
+ *
+ * @see EbmlProcessor#floatElement(int, double)
+ */
+ @CallSuper
+ protected void floatElement(int id, double value) throws ParserException {
+ switch (id) {
+ case ID_DURATION:
+ durationTimecode = (long) value;
+ break;
+ case ID_SAMPLING_FREQUENCY:
+ currentTrack.sampleRate = (int) value;
+ break;
+ case ID_PRIMARY_R_CHROMATICITY_X:
+ currentTrack.primaryRChromaticityX = (float) value;
+ break;
+ case ID_PRIMARY_R_CHROMATICITY_Y:
+ currentTrack.primaryRChromaticityY = (float) value;
+ break;
+ case ID_PRIMARY_G_CHROMATICITY_X:
+ currentTrack.primaryGChromaticityX = (float) value;
+ break;
+ case ID_PRIMARY_G_CHROMATICITY_Y:
+ currentTrack.primaryGChromaticityY = (float) value;
+ break;
+ case ID_PRIMARY_B_CHROMATICITY_X:
+ currentTrack.primaryBChromaticityX = (float) value;
+ break;
+ case ID_PRIMARY_B_CHROMATICITY_Y:
+ currentTrack.primaryBChromaticityY = (float) value;
+ break;
+ case ID_WHITE_POINT_CHROMATICITY_X:
+ currentTrack.whitePointChromaticityX = (float) value;
+ break;
+ case ID_WHITE_POINT_CHROMATICITY_Y:
+ currentTrack.whitePointChromaticityY = (float) value;
+ break;
+ case ID_LUMNINANCE_MAX:
+ currentTrack.maxMasteringLuminance = (float) value;
+ break;
+ case ID_LUMNINANCE_MIN:
+ currentTrack.minMasteringLuminance = (float) value;
+ break;
+ case ID_PROJECTION_POSE_YAW:
+ currentTrack.projectionPoseYaw = (float) value;
+ break;
+ case ID_PROJECTION_POSE_PITCH:
+ currentTrack.projectionPosePitch = (float) value;
+ break;
+ case ID_PROJECTION_POSE_ROLL:
+ currentTrack.projectionPoseRoll = (float) value;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Called when a string element is encountered.
+ *
+ * @see EbmlProcessor#stringElement(int, String)
+ */
+ @CallSuper
+ protected void stringElement(int id, String value) throws ParserException {
+ switch (id) {
+ case ID_DOC_TYPE:
+ // Validate that DocType is supported.
+ if (!DOC_TYPE_WEBM.equals(value) && !DOC_TYPE_MATROSKA.equals(value)) {
+ throw new ParserException("DocType " + value + " not supported");
+ }
+ break;
+ case ID_NAME:
+ currentTrack.name = value;
+ break;
+ case ID_CODEC_ID:
+ currentTrack.codecId = value;
+ break;
+ case ID_LANGUAGE:
+ currentTrack.language = value;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Called when a binary element is encountered.
+ *
+ * @see EbmlProcessor#binaryElement(int, int, ExtractorInput)
+ */
+ @CallSuper
+ protected void binaryElement(int id, int contentSize, ExtractorInput input)
+ throws IOException, InterruptedException {
+ switch (id) {
+ case ID_SEEK_ID:
+ Arrays.fill(seekEntryIdBytes.data, (byte) 0);
+ input.readFully(seekEntryIdBytes.data, 4 - contentSize, contentSize);
+ seekEntryIdBytes.setPosition(0);
+ seekEntryId = (int) seekEntryIdBytes.readUnsignedInt();
+ break;
+ case ID_CODEC_PRIVATE:
+ currentTrack.codecPrivate = new byte[contentSize];
+ input.readFully(currentTrack.codecPrivate, 0, contentSize);
+ break;
+ case ID_PROJECTION_PRIVATE:
+ currentTrack.projectionData = new byte[contentSize];
+ input.readFully(currentTrack.projectionData, 0, contentSize);
+ break;
+ case ID_CONTENT_COMPRESSION_SETTINGS:
+ // This extractor only supports header stripping, so the payload is the stripped bytes.
+ currentTrack.sampleStrippedBytes = new byte[contentSize];
+ input.readFully(currentTrack.sampleStrippedBytes, 0, contentSize);
+ break;
+ case ID_CONTENT_ENCRYPTION_KEY_ID:
+ byte[] encryptionKey = new byte[contentSize];
+ input.readFully(encryptionKey, 0, contentSize);
+ currentTrack.cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionKey,
+ 0, 0); // We assume patternless AES-CTR.
+ break;
+ case ID_SIMPLE_BLOCK:
+ case ID_BLOCK:
+ // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
+ // and http://matroska.org/technical/specs/index.html#block_structure
+ // for info about how data is organized in SimpleBlock and Block elements respectively. They
+ // differ only in the way flags are specified.
+
+ if (blockState == BLOCK_STATE_START) {
+ blockTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true, 8);
+ blockTrackNumberLength = varintReader.getLastLength();
+ blockDurationUs = C.TIME_UNSET;
+ blockState = BLOCK_STATE_HEADER;
+ scratch.reset();
+ }
+
+ Track track = tracks.get(blockTrackNumber);
+
+ // Ignore the block if we don't know about the track to which it belongs.
+ if (track == null) {
+ input.skipFully(contentSize - blockTrackNumberLength);
+ blockState = BLOCK_STATE_START;
+ return;
+ }
+
+ if (blockState == BLOCK_STATE_HEADER) {
+ // Read the relative timecode (2 bytes) and flags (1 byte).
+ readScratch(input, 3);
+ int lacing = (scratch.data[2] & 0x06) >> 1;
+ if (lacing == LACING_NONE) {
+ blockSampleCount = 1;
+ blockSampleSizes = ensureArrayCapacity(blockSampleSizes, 1);
+ blockSampleSizes[0] = contentSize - blockTrackNumberLength - 3;
+ } else {
+ // Read the sample count (1 byte).
+ readScratch(input, 4);
+ blockSampleCount = (scratch.data[3] & 0xFF) + 1;
+ blockSampleSizes = ensureArrayCapacity(blockSampleSizes, blockSampleCount);
+ if (lacing == LACING_FIXED_SIZE) {
+ int blockLacingSampleSize =
+ (contentSize - blockTrackNumberLength - 4) / blockSampleCount;
+ Arrays.fill(blockSampleSizes, 0, blockSampleCount, blockLacingSampleSize);
+ } else if (lacing == LACING_XIPH) {
+ int totalSamplesSize = 0;
+ int headerSize = 4;
+ for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) {
+ blockSampleSizes[sampleIndex] = 0;
+ int byteValue;
+ do {
+ readScratch(input, ++headerSize);
+ byteValue = scratch.data[headerSize - 1] & 0xFF;
+ blockSampleSizes[sampleIndex] += byteValue;
+ } while (byteValue == 0xFF);
+ totalSamplesSize += blockSampleSizes[sampleIndex];
+ }
+ blockSampleSizes[blockSampleCount - 1] =
+ contentSize - blockTrackNumberLength - headerSize - totalSamplesSize;
+ } else if (lacing == LACING_EBML) {
+ int totalSamplesSize = 0;
+ int headerSize = 4;
+ for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) {
+ blockSampleSizes[sampleIndex] = 0;
+ readScratch(input, ++headerSize);
+ if (scratch.data[headerSize - 1] == 0) {
+ throw new ParserException("No valid varint length mask found");
+ }
+ long readValue = 0;
+ for (int i = 0; i < 8; i++) {
+ int lengthMask = 1 << (7 - i);
+ if ((scratch.data[headerSize - 1] & lengthMask) != 0) {
+ int readPosition = headerSize - 1;
+ headerSize += i;
+ readScratch(input, headerSize);
+ readValue = (scratch.data[readPosition++] & 0xFF) & ~lengthMask;
+ while (readPosition < headerSize) {
+ readValue <<= 8;
+ readValue |= (scratch.data[readPosition++] & 0xFF);
+ }
+ // The first read value is the first size. Later values are signed offsets.
+ if (sampleIndex > 0) {
+ readValue -= (1L << (6 + i * 7)) - 1;
+ }
+ break;
+ }
+ }
+ if (readValue < Integer.MIN_VALUE || readValue > Integer.MAX_VALUE) {
+ throw new ParserException("EBML lacing sample size out of range.");
+ }
+ int intReadValue = (int) readValue;
+ blockSampleSizes[sampleIndex] =
+ sampleIndex == 0
+ ? intReadValue
+ : blockSampleSizes[sampleIndex - 1] + intReadValue;
+ totalSamplesSize += blockSampleSizes[sampleIndex];
+ }
+ blockSampleSizes[blockSampleCount - 1] =
+ contentSize - blockTrackNumberLength - headerSize - totalSamplesSize;
+ } else {
+ // Lacing is always in the range 0--3.
+ throw new ParserException("Unexpected lacing value: " + lacing);
+ }
+ }
+
+ int timecode = (scratch.data[0] << 8) | (scratch.data[1] & 0xFF);
+ blockTimeUs = clusterTimecodeUs + scaleTimecodeToUs(timecode);
+ boolean isInvisible = (scratch.data[2] & 0x08) == 0x08;
+ boolean isKeyframe = track.type == TRACK_TYPE_AUDIO
+ || (id == ID_SIMPLE_BLOCK && (scratch.data[2] & 0x80) == 0x80);
+ blockFlags = (isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0)
+ | (isInvisible ? C.BUFFER_FLAG_DECODE_ONLY : 0);
+ blockState = BLOCK_STATE_DATA;
+ blockSampleIndex = 0;
+ }
+
+ if (id == ID_SIMPLE_BLOCK) {
+ // For SimpleBlock, we can write sample data and immediately commit the corresponding
+ // sample metadata.
+ while (blockSampleIndex < blockSampleCount) {
+ int sampleSize = writeSampleData(input, track, blockSampleSizes[blockSampleIndex]);
+ long sampleTimeUs =
+ blockTimeUs + (blockSampleIndex * track.defaultSampleDurationNs) / 1000;
+ commitSampleToOutput(track, sampleTimeUs, blockFlags, sampleSize, /* offset= */ 0);
+ blockSampleIndex++;
+ }
+ blockState = BLOCK_STATE_START;
+ } else {
+ // For Block, we need to wait until the end of the BlockGroup element before committing
+ // sample metadata. This is so that we can handle ReferenceBlock (which can be used to
+ // infer whether the first sample in the block is a keyframe), and BlockAdditions (which
+ // can contain additional sample data to append) contained in the block group. Just output
+ // the sample data, storing the final sample sizes for when we commit the metadata.
+ while (blockSampleIndex < blockSampleCount) {
+ blockSampleSizes[blockSampleIndex] =
+ writeSampleData(input, track, blockSampleSizes[blockSampleIndex]);
+ blockSampleIndex++;
+ }
+ }
+
+ break;
+ case ID_BLOCK_ADDITIONAL:
+ if (blockState != BLOCK_STATE_DATA) {
+ return;
+ }
+ handleBlockAdditionalData(
+ tracks.get(blockTrackNumber), blockAdditionalId, input, contentSize);
+ break;
+ default:
+ throw new ParserException("Unexpected id: " + id);
+ }
+ }
+
+ protected void handleBlockAdditionalData(
+ Track track, int blockAdditionalId, ExtractorInput input, int contentSize)
+ throws IOException, InterruptedException {
+ if (blockAdditionalId == BLOCK_ADDITIONAL_ID_VP9_ITU_T_35
+ && CODEC_ID_VP9.equals(track.codecId)) {
+ blockAdditionalData.reset(contentSize);
+ input.readFully(blockAdditionalData.data, 0, contentSize);
+ } else {
+ // Unhandled block additional data.
+ input.skipFully(contentSize);
+ }
+ }
+
+ private void commitSampleToOutput(
+ Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) {
+ if (track.trueHdSampleRechunker != null) {
+ track.trueHdSampleRechunker.sampleMetadata(track, timeUs, flags, size, offset);
+ } else {
+ if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) {
+ if (blockSampleCount > 1) {
+ Log.w(TAG, "Skipping subtitle sample in laced block.");
+ } else if (blockDurationUs == C.TIME_UNSET) {
+ Log.w(TAG, "Skipping subtitle sample with no duration.");
+ } else {
+ setSubtitleEndTime(track.codecId, blockDurationUs, subtitleSample.data);
+ // Note: If we ever want to support DRM protected subtitles then we'll need to output the
+ // appropriate encryption data here.
+ track.output.sampleData(subtitleSample, subtitleSample.limit());
+ size += subtitleSample.limit();
+ }
+ }
+
+ if ((flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) {
+ if (blockSampleCount > 1) {
+ // There were multiple samples in the block. Appending the additional data to the last
+ // sample doesn't make sense. Skip instead.
+ flags &= ~C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA;
+ } else {
+ // Append supplemental data.
+ int blockAdditionalSize = blockAdditionalData.limit();
+ track.output.sampleData(blockAdditionalData, blockAdditionalSize);
+ size += blockAdditionalSize;
+ }
+ }
+ track.output.sampleMetadata(timeUs, flags, size, offset, track.cryptoData);
+ }
+ haveOutputSample = true;
+ }
+
+ /**
+ * Ensures {@link #scratch} contains at least {@code requiredLength} bytes of data, reading from
+ * the extractor input if necessary.
+ */
+ private void readScratch(ExtractorInput input, int requiredLength)
+ throws IOException, InterruptedException {
+ if (scratch.limit() >= requiredLength) {
+ return;
+ }
+ if (scratch.capacity() < requiredLength) {
+ scratch.reset(Arrays.copyOf(scratch.data, Math.max(scratch.data.length * 2, requiredLength)),
+ scratch.limit());
+ }
+ input.readFully(scratch.data, scratch.limit(), requiredLength - scratch.limit());
+ scratch.setLimit(requiredLength);
+ }
+
+ /**
+ * Writes data for a single sample to the track output.
+ *
+ * @param input The input from which to read sample data.
+ * @param track The track to output the sample to.
+ * @param size The size of the sample data on the input side.
+ * @return The final size of the written sample.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private int writeSampleData(ExtractorInput input, Track track, int size)
+ throws IOException, InterruptedException {
+ if (CODEC_ID_SUBRIP.equals(track.codecId)) {
+ writeSubtitleSampleData(input, SUBRIP_PREFIX, size);
+ return finishWriteSampleData();
+ } else if (CODEC_ID_ASS.equals(track.codecId)) {
+ writeSubtitleSampleData(input, SSA_PREFIX, size);
+ return finishWriteSampleData();
+ }
+
+ TrackOutput output = track.output;
+ if (!sampleEncodingHandled) {
+ if (track.hasContentEncryption) {
+ // If the sample is encrypted, read its encryption signal byte and set the IV size.
+ // Clear the encrypted flag.
+ blockFlags &= ~C.BUFFER_FLAG_ENCRYPTED;
+ if (!sampleSignalByteRead) {
+ input.readFully(scratch.data, 0, 1);
+ sampleBytesRead++;
+ if ((scratch.data[0] & 0x80) == 0x80) {
+ throw new ParserException("Extension bit is set in signal byte");
+ }
+ sampleSignalByte = scratch.data[0];
+ sampleSignalByteRead = true;
+ }
+ boolean isEncrypted = (sampleSignalByte & 0x01) == 0x01;
+ if (isEncrypted) {
+ boolean hasSubsampleEncryption = (sampleSignalByte & 0x02) == 0x02;
+ blockFlags |= C.BUFFER_FLAG_ENCRYPTED;
+ if (!sampleInitializationVectorRead) {
+ input.readFully(encryptionInitializationVector.data, 0, ENCRYPTION_IV_SIZE);
+ sampleBytesRead += ENCRYPTION_IV_SIZE;
+ sampleInitializationVectorRead = true;
+ // Write the signal byte, containing the IV size and the subsample encryption flag.
+ scratch.data[0] = (byte) (ENCRYPTION_IV_SIZE | (hasSubsampleEncryption ? 0x80 : 0x00));
+ scratch.setPosition(0);
+ output.sampleData(scratch, 1);
+ sampleBytesWritten++;
+ // Write the IV.
+ encryptionInitializationVector.setPosition(0);
+ output.sampleData(encryptionInitializationVector, ENCRYPTION_IV_SIZE);
+ sampleBytesWritten += ENCRYPTION_IV_SIZE;
+ }
+ if (hasSubsampleEncryption) {
+ if (!samplePartitionCountRead) {
+ input.readFully(scratch.data, 0, 1);
+ sampleBytesRead++;
+ scratch.setPosition(0);
+ samplePartitionCount = scratch.readUnsignedByte();
+ samplePartitionCountRead = true;
+ }
+ int samplePartitionDataSize = samplePartitionCount * 4;
+ scratch.reset(samplePartitionDataSize);
+ input.readFully(scratch.data, 0, samplePartitionDataSize);
+ sampleBytesRead += samplePartitionDataSize;
+ short subsampleCount = (short) (1 + (samplePartitionCount / 2));
+ int subsampleDataSize = 2 + 6 * subsampleCount;
+ if (encryptionSubsampleDataBuffer == null
+ || encryptionSubsampleDataBuffer.capacity() < subsampleDataSize) {
+ encryptionSubsampleDataBuffer = ByteBuffer.allocate(subsampleDataSize);
+ }
+ encryptionSubsampleDataBuffer.position(0);
+ encryptionSubsampleDataBuffer.putShort(subsampleCount);
+ // Loop through the partition offsets and write out the data in the way ExoPlayer
+ // wants it (ISO 23001-7 Part 7):
+ // 2 bytes - sub sample count.
+ // for each sub sample:
+ // 2 bytes - clear data size.
+ // 4 bytes - encrypted data size.
+ int partitionOffset = 0;
+ for (int i = 0; i < samplePartitionCount; i++) {
+ int previousPartitionOffset = partitionOffset;
+ partitionOffset = scratch.readUnsignedIntToInt();
+ if ((i % 2) == 0) {
+ encryptionSubsampleDataBuffer.putShort(
+ (short) (partitionOffset - previousPartitionOffset));
+ } else {
+ encryptionSubsampleDataBuffer.putInt(partitionOffset - previousPartitionOffset);
+ }
+ }
+ int finalPartitionSize = size - sampleBytesRead - partitionOffset;
+ if ((samplePartitionCount % 2) == 1) {
+ encryptionSubsampleDataBuffer.putInt(finalPartitionSize);
+ } else {
+ encryptionSubsampleDataBuffer.putShort((short) finalPartitionSize);
+ encryptionSubsampleDataBuffer.putInt(0);
+ }
+ encryptionSubsampleData.reset(encryptionSubsampleDataBuffer.array(), subsampleDataSize);
+ output.sampleData(encryptionSubsampleData, subsampleDataSize);
+ sampleBytesWritten += subsampleDataSize;
+ }
+ }
+ } else if (track.sampleStrippedBytes != null) {
+ // If the sample has header stripping, prepare to read/output the stripped bytes first.
+ sampleStrippedBytes.reset(track.sampleStrippedBytes, track.sampleStrippedBytes.length);
+ }
+
+ if (track.maxBlockAdditionId > 0) {
+ blockFlags |= C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA;
+ blockAdditionalData.reset();
+ // If there is supplemental data, the structure of the sample data is:
+ // sample size (4 bytes) || sample data || supplemental data
+ scratch.reset(/* limit= */ 4);
+ scratch.data[0] = (byte) ((size >> 24) & 0xFF);
+ scratch.data[1] = (byte) ((size >> 16) & 0xFF);
+ scratch.data[2] = (byte) ((size >> 8) & 0xFF);
+ scratch.data[3] = (byte) (size & 0xFF);
+ output.sampleData(scratch, 4);
+ sampleBytesWritten += 4;
+ }
+
+ sampleEncodingHandled = true;
+ }
+ size += sampleStrippedBytes.limit();
+
+ if (CODEC_ID_H264.equals(track.codecId) || CODEC_ID_H265.equals(track.codecId)) {
+ // TODO: Deduplicate with Mp4Extractor.
+
+ // 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[] nalLengthData = nalLength.data;
+ nalLengthData[0] = 0;
+ nalLengthData[1] = 0;
+ nalLengthData[2] = 0;
+ int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength;
+ 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 (sampleBytesRead < size) {
+ if (sampleCurrentNalBytesRemaining == 0) {
+ // Read the NAL length so that we know where we find the next one.
+ writeToTarget(
+ input, nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
+ sampleBytesRead += nalUnitLengthFieldLength;
+ nalLength.setPosition(0);
+ sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
+ // Write a start code for the current NAL unit.
+ nalStartCode.setPosition(0);
+ output.sampleData(nalStartCode, 4);
+ sampleBytesWritten += 4;
+ } else {
+ // Write the payload of the NAL unit.
+ int bytesWritten = writeToOutput(input, output, sampleCurrentNalBytesRemaining);
+ sampleBytesRead += bytesWritten;
+ sampleBytesWritten += bytesWritten;
+ sampleCurrentNalBytesRemaining -= bytesWritten;
+ }
+ }
+ } else {
+ if (track.trueHdSampleRechunker != null) {
+ Assertions.checkState(sampleStrippedBytes.limit() == 0);
+ track.trueHdSampleRechunker.startSample(input);
+ }
+ while (sampleBytesRead < size) {
+ int bytesWritten = writeToOutput(input, output, size - sampleBytesRead);
+ sampleBytesRead += bytesWritten;
+ sampleBytesWritten += bytesWritten;
+ }
+ }
+
+ if (CODEC_ID_VORBIS.equals(track.codecId)) {
+ // Vorbis decoder in android MediaCodec [1] expects the last 4 bytes of the sample to be the
+ // number of samples in the current page. This definition holds good only for Ogg and
+ // irrelevant for Matroska. So we always set this to -1 (the decoder will ignore this value if
+ // we set it to -1). The android platform media extractor [2] does the same.
+ // [1] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/codecs/vorbis/dec/SoftVorbis.cpp#314
+ // [2] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/NuMediaExtractor.cpp#474
+ vorbisNumPageSamples.setPosition(0);
+ output.sampleData(vorbisNumPageSamples, 4);
+ sampleBytesWritten += 4;
+ }
+
+ return finishWriteSampleData();
+ }
+
+ /**
+ * Called by {@link #writeSampleData(ExtractorInput, Track, int)} when the sample has been
+ * written. Returns the final sample size and resets state for the next sample.
+ */
+ private int finishWriteSampleData() {
+ int sampleSize = sampleBytesWritten;
+ resetWriteSampleData();
+ return sampleSize;
+ }
+
+ /** Resets state used by {@link #writeSampleData(ExtractorInput, Track, int)}. */
+ private void resetWriteSampleData() {
+ sampleBytesRead = 0;
+ sampleBytesWritten = 0;
+ sampleCurrentNalBytesRemaining = 0;
+ sampleEncodingHandled = false;
+ sampleSignalByteRead = false;
+ samplePartitionCountRead = false;
+ samplePartitionCount = 0;
+ sampleSignalByte = (byte) 0;
+ sampleInitializationVectorRead = false;
+ sampleStrippedBytes.reset();
+ }
+
+ private void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, int size)
+ throws IOException, InterruptedException {
+ int sizeWithPrefix = samplePrefix.length + size;
+ if (subtitleSample.capacity() < sizeWithPrefix) {
+ // Initialize subripSample to contain the required prefix and have space to hold a subtitle
+ // twice as long as this one.
+ subtitleSample.data = Arrays.copyOf(samplePrefix, sizeWithPrefix + size);
+ } else {
+ System.arraycopy(samplePrefix, 0, subtitleSample.data, 0, samplePrefix.length);
+ }
+ input.readFully(subtitleSample.data, samplePrefix.length, size);
+ subtitleSample.reset(sizeWithPrefix);
+ // Defer writing the data to the track output. We need to modify the sample data by setting
+ // the correct end timecode, which we might not have yet.
+ }
+
+ /**
+ * Overwrites the end timecode in {@code subtitleData} with the correctly formatted time derived
+ * from {@code durationUs}.
+ *
+ * <p>See documentation on {@link #SSA_DIALOGUE_FORMAT} and {@link #SUBRIP_PREFIX} for why we use
+ * the duration as the end timecode.
+ *
+ * @param codecId The subtitle codec; must be {@link #CODEC_ID_SUBRIP} or {@link #CODEC_ID_ASS}.
+ * @param durationUs The duration of the sample, in microseconds.
+ * @param subtitleData The subtitle sample in which to overwrite the end timecode (output
+ * parameter).
+ */
+ private static void setSubtitleEndTime(String codecId, long durationUs, byte[] subtitleData) {
+ byte[] endTimecode;
+ int endTimecodeOffset;
+ switch (codecId) {
+ case CODEC_ID_SUBRIP:
+ endTimecode =
+ formatSubtitleTimecode(
+ durationUs, SUBRIP_TIMECODE_FORMAT, SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR);
+ endTimecodeOffset = SUBRIP_PREFIX_END_TIMECODE_OFFSET;
+ break;
+ case CODEC_ID_ASS:
+ endTimecode =
+ formatSubtitleTimecode(
+ durationUs, SSA_TIMECODE_FORMAT, SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR);
+ endTimecodeOffset = SSA_PREFIX_END_TIMECODE_OFFSET;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ System.arraycopy(endTimecode, 0, subtitleData, endTimecodeOffset, endTimecode.length);
+ }
+
+ /**
+ * Formats {@code timeUs} using {@code timecodeFormat}, and sets it as the end timecode in {@code
+ * subtitleSampleData}.
+ */
+ private static byte[] formatSubtitleTimecode(
+ long timeUs, String timecodeFormat, long lastTimecodeValueScalingFactor) {
+ Assertions.checkArgument(timeUs != C.TIME_UNSET);
+ byte[] timeCodeData;
+ int hours = (int) (timeUs / (3600 * C.MICROS_PER_SECOND));
+ timeUs -= (hours * 3600 * C.MICROS_PER_SECOND);
+ int minutes = (int) (timeUs / (60 * C.MICROS_PER_SECOND));
+ timeUs -= (minutes * 60 * C.MICROS_PER_SECOND);
+ int seconds = (int) (timeUs / C.MICROS_PER_SECOND);
+ timeUs -= (seconds * C.MICROS_PER_SECOND);
+ int lastValue = (int) (timeUs / lastTimecodeValueScalingFactor);
+ timeCodeData =
+ Util.getUtf8Bytes(
+ String.format(Locale.US, timecodeFormat, hours, minutes, seconds, lastValue));
+ return timeCodeData;
+ }
+
+ /**
+ * Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of
+ * pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}.
+ */
+ private void writeToTarget(ExtractorInput input, byte[] target, int offset, int length)
+ throws IOException, InterruptedException {
+ int pendingStrippedBytes = Math.min(length, sampleStrippedBytes.bytesLeft());
+ input.readFully(target, offset + pendingStrippedBytes, length - pendingStrippedBytes);
+ if (pendingStrippedBytes > 0) {
+ sampleStrippedBytes.readBytes(target, offset, pendingStrippedBytes);
+ }
+ }
+
+ /**
+ * Outputs up to {@code length} bytes of sample data to {@code output}, consisting of either
+ * {@link #sampleStrippedBytes} or data read from {@code input}.
+ */
+ private int writeToOutput(ExtractorInput input, TrackOutput output, int length)
+ throws IOException, InterruptedException {
+ int bytesWritten;
+ int strippedBytesLeft = sampleStrippedBytes.bytesLeft();
+ if (strippedBytesLeft > 0) {
+ bytesWritten = Math.min(length, strippedBytesLeft);
+ output.sampleData(sampleStrippedBytes, bytesWritten);
+ } else {
+ bytesWritten = output.sampleData(input, length, false);
+ }
+ return bytesWritten;
+ }
+
+ /**
+ * Builds a {@link SeekMap} from the recently gathered Cues information.
+ *
+ * @return The built {@link SeekMap}. The returned {@link SeekMap} may be unseekable if cues
+ * information was missing or incomplete.
+ */
+ private SeekMap buildSeekMap() {
+ if (segmentContentPosition == C.POSITION_UNSET || durationUs == C.TIME_UNSET
+ || cueTimesUs == null || cueTimesUs.size() == 0
+ || cueClusterPositions == null || cueClusterPositions.size() != cueTimesUs.size()) {
+ // Cues information is missing or incomplete.
+ cueTimesUs = null;
+ cueClusterPositions = null;
+ return new SeekMap.Unseekable(durationUs);
+ }
+ int cuePointsSize = cueTimesUs.size();
+ int[] sizes = new int[cuePointsSize];
+ long[] offsets = new long[cuePointsSize];
+ long[] durationsUs = new long[cuePointsSize];
+ long[] timesUs = new long[cuePointsSize];
+ for (int i = 0; i < cuePointsSize; i++) {
+ timesUs[i] = cueTimesUs.get(i);
+ offsets[i] = segmentContentPosition + cueClusterPositions.get(i);
+ }
+ for (int i = 0; i < cuePointsSize - 1; i++) {
+ sizes[i] = (int) (offsets[i + 1] - offsets[i]);
+ durationsUs[i] = timesUs[i + 1] - timesUs[i];
+ }
+ sizes[cuePointsSize - 1] =
+ (int) (segmentContentPosition + segmentContentSize - offsets[cuePointsSize - 1]);
+ durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
+
+ long lastDurationUs = durationsUs[cuePointsSize - 1];
+ if (lastDurationUs <= 0) {
+ Log.w(TAG, "Discarding last cue point with unexpected duration: " + lastDurationUs);
+ sizes = Arrays.copyOf(sizes, sizes.length - 1);
+ offsets = Arrays.copyOf(offsets, offsets.length - 1);
+ durationsUs = Arrays.copyOf(durationsUs, durationsUs.length - 1);
+ timesUs = Arrays.copyOf(timesUs, timesUs.length - 1);
+ }
+
+ cueTimesUs = null;
+ cueClusterPositions = null;
+ return new ChunkIndex(sizes, offsets, durationsUs, timesUs);
+ }
+
+ /**
+ * Updates the position of the holder to Cues element's position if the extractor configuration
+ * permits use of master seek entry. After building Cues sets the holder's position back to where
+ * it was before.
+ *
+ * @param seekPosition The holder whose position will be updated.
+ * @param currentPosition Current position of the input.
+ * @return Whether the seek position was updated.
+ */
+ private boolean maybeSeekForCues(PositionHolder seekPosition, long currentPosition) {
+ if (seekForCues) {
+ seekPositionAfterBuildingCues = currentPosition;
+ seekPosition.position = cuesContentPosition;
+ seekForCues = false;
+ return true;
+ }
+ // After parsing Cues, seek back to original position if available. We will not do this unless
+ // we seeked to get to the Cues in the first place.
+ if (sentSeekMap && seekPositionAfterBuildingCues != C.POSITION_UNSET) {
+ seekPosition.position = seekPositionAfterBuildingCues;
+ seekPositionAfterBuildingCues = C.POSITION_UNSET;
+ return true;
+ }
+ return false;
+ }
+
+ private long scaleTimecodeToUs(long unscaledTimecode) throws ParserException {
+ if (timecodeScale == C.TIME_UNSET) {
+ throw new ParserException("Can't scale timecode prior to timecodeScale being set.");
+ }
+ return Util.scaleLargeTimestamp(unscaledTimecode, timecodeScale, 1000);
+ }
+
+ private static boolean isCodecSupported(String codecId) {
+ return CODEC_ID_VP8.equals(codecId)
+ || CODEC_ID_VP9.equals(codecId)
+ || CODEC_ID_AV1.equals(codecId)
+ || CODEC_ID_MPEG2.equals(codecId)
+ || CODEC_ID_MPEG4_SP.equals(codecId)
+ || CODEC_ID_MPEG4_ASP.equals(codecId)
+ || CODEC_ID_MPEG4_AP.equals(codecId)
+ || CODEC_ID_H264.equals(codecId)
+ || CODEC_ID_H265.equals(codecId)
+ || CODEC_ID_FOURCC.equals(codecId)
+ || CODEC_ID_THEORA.equals(codecId)
+ || CODEC_ID_OPUS.equals(codecId)
+ || CODEC_ID_VORBIS.equals(codecId)
+ || CODEC_ID_AAC.equals(codecId)
+ || CODEC_ID_MP2.equals(codecId)
+ || CODEC_ID_MP3.equals(codecId)
+ || CODEC_ID_AC3.equals(codecId)
+ || CODEC_ID_E_AC3.equals(codecId)
+ || CODEC_ID_TRUEHD.equals(codecId)
+ || CODEC_ID_DTS.equals(codecId)
+ || CODEC_ID_DTS_EXPRESS.equals(codecId)
+ || CODEC_ID_DTS_LOSSLESS.equals(codecId)
+ || CODEC_ID_FLAC.equals(codecId)
+ || CODEC_ID_ACM.equals(codecId)
+ || CODEC_ID_PCM_INT_LIT.equals(codecId)
+ || CODEC_ID_SUBRIP.equals(codecId)
+ || CODEC_ID_ASS.equals(codecId)
+ || CODEC_ID_VOBSUB.equals(codecId)
+ || CODEC_ID_PGS.equals(codecId)
+ || CODEC_ID_DVBSUB.equals(codecId);
+ }
+
+ /**
+ * Returns an array that can store (at least) {@code length} elements, which will be either a new
+ * array or {@code array} if it's not null and large enough.
+ */
+ private static int[] ensureArrayCapacity(int[] array, int length) {
+ if (array == null) {
+ return new int[length];
+ } else if (array.length >= length) {
+ return array;
+ } else {
+ // Double the size to avoid allocating constantly if the required length increases gradually.
+ return new int[Math.max(array.length * 2, length)];
+ }
+ }
+
+ /** Passes events through to the outer {@link MatroskaExtractor}. */
+ private final class InnerEbmlProcessor implements EbmlProcessor {
+
+ @Override
+ @ElementType
+ public int getElementType(int id) {
+ return MatroskaExtractor.this.getElementType(id);
+ }
+
+ @Override
+ public boolean isLevel1Element(int id) {
+ return MatroskaExtractor.this.isLevel1Element(id);
+ }
+
+ @Override
+ public void startMasterElement(int id, long contentPosition, long contentSize)
+ throws ParserException {
+ MatroskaExtractor.this.startMasterElement(id, contentPosition, contentSize);
+ }
+
+ @Override
+ public void endMasterElement(int id) throws ParserException {
+ MatroskaExtractor.this.endMasterElement(id);
+ }
+
+ @Override
+ public void integerElement(int id, long value) throws ParserException {
+ MatroskaExtractor.this.integerElement(id, value);
+ }
+
+ @Override
+ public void floatElement(int id, double value) throws ParserException {
+ MatroskaExtractor.this.floatElement(id, value);
+ }
+
+ @Override
+ public void stringElement(int id, String value) throws ParserException {
+ MatroskaExtractor.this.stringElement(id, value);
+ }
+
+ @Override
+ public void binaryElement(int id, int contentsSize, ExtractorInput input)
+ throws IOException, InterruptedException {
+ MatroskaExtractor.this.binaryElement(id, contentsSize, input);
+ }
+ }
+
+ /**
+ * Rechunks TrueHD sample data into groups of {@link Ac3Util#TRUEHD_RECHUNK_SAMPLE_COUNT} samples.
+ */
+ private static final class TrueHdSampleRechunker {
+
+ private final byte[] syncframePrefix;
+
+ private boolean foundSyncframe;
+ private int chunkSampleCount;
+ private long chunkTimeUs;
+ private @C.BufferFlags int chunkFlags;
+ private int chunkSize;
+ private int chunkOffset;
+
+ public TrueHdSampleRechunker() {
+ syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH];
+ }
+
+ public void reset() {
+ foundSyncframe = false;
+ chunkSampleCount = 0;
+ }
+
+ public void startSample(ExtractorInput input) throws IOException, InterruptedException {
+ if (foundSyncframe) {
+ return;
+ }
+ input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH);
+ input.resetPeekPosition();
+ if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) {
+ return;
+ }
+ foundSyncframe = true;
+ }
+
+ public void sampleMetadata(
+ Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) {
+ if (!foundSyncframe) {
+ return;
+ }
+ if (chunkSampleCount++ == 0) {
+ // This is the first sample in the chunk.
+ chunkTimeUs = timeUs;
+ chunkFlags = flags;
+ chunkSize = 0;
+ }
+ chunkSize += size;
+ chunkOffset = offset; // The offset is to the end of the sample.
+ if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) {
+ outputPendingSampleMetadata(track);
+ }
+ }
+
+ public void outputPendingSampleMetadata(Track track) {
+ if (chunkSampleCount > 0) {
+ track.output.sampleMetadata(
+ chunkTimeUs, chunkFlags, chunkSize, chunkOffset, track.cryptoData);
+ chunkSampleCount = 0;
+ }
+ }
+ }
+
+ private static final class Track {
+
+ private static final int DISPLAY_UNIT_PIXELS = 0;
+ private static final int MAX_CHROMATICITY = 50000; // Defined in CTA-861.3.
+ /**
+ * Default max content light level (CLL) that should be encoded into hdrStaticInfo.
+ */
+ private static final int DEFAULT_MAX_CLL = 1000; // nits.
+
+ /**
+ * Default frame-average light level (FALL) that should be encoded into hdrStaticInfo.
+ */
+ private static final int DEFAULT_MAX_FALL = 200; // nits.
+
+ // Common elements.
+ public String name;
+ public String codecId;
+ public int number;
+ public int type;
+ public int defaultSampleDurationNs;
+ public int maxBlockAdditionId;
+ public boolean hasContentEncryption;
+ public byte[] sampleStrippedBytes;
+ public TrackOutput.CryptoData cryptoData;
+ public byte[] codecPrivate;
+ public DrmInitData drmInitData;
+
+ // Video elements.
+ public int width = Format.NO_VALUE;
+ public int height = Format.NO_VALUE;
+ public int displayWidth = Format.NO_VALUE;
+ public int displayHeight = Format.NO_VALUE;
+ public int displayUnit = DISPLAY_UNIT_PIXELS;
+ @C.Projection public int projectionType = Format.NO_VALUE;
+ public float projectionPoseYaw = 0f;
+ public float projectionPosePitch = 0f;
+ public float projectionPoseRoll = 0f;
+ public byte[] projectionData = null;
+ @C.StereoMode
+ public int stereoMode = Format.NO_VALUE;
+ public boolean hasColorInfo = false;
+ @C.ColorSpace
+ public int colorSpace = Format.NO_VALUE;
+ @C.ColorTransfer
+ public int colorTransfer = Format.NO_VALUE;
+ @C.ColorRange
+ public int colorRange = Format.NO_VALUE;
+ public int maxContentLuminance = DEFAULT_MAX_CLL;
+ public int maxFrameAverageLuminance = DEFAULT_MAX_FALL;
+ public float primaryRChromaticityX = Format.NO_VALUE;
+ public float primaryRChromaticityY = Format.NO_VALUE;
+ public float primaryGChromaticityX = Format.NO_VALUE;
+ public float primaryGChromaticityY = Format.NO_VALUE;
+ public float primaryBChromaticityX = Format.NO_VALUE;
+ public float primaryBChromaticityY = Format.NO_VALUE;
+ public float whitePointChromaticityX = Format.NO_VALUE;
+ public float whitePointChromaticityY = Format.NO_VALUE;
+ public float maxMasteringLuminance = Format.NO_VALUE;
+ public float minMasteringLuminance = Format.NO_VALUE;
+
+ // Audio elements. Initially set to their default values.
+ public int channelCount = 1;
+ public int audioBitDepth = Format.NO_VALUE;
+ public int sampleRate = 8000;
+ public long codecDelayNs = 0;
+ public long seekPreRollNs = 0;
+ @Nullable public TrueHdSampleRechunker trueHdSampleRechunker;
+
+ // Text elements.
+ public boolean flagForced;
+ public boolean flagDefault = true;
+ private String language = "eng";
+
+ // Set when the output is initialized. nalUnitLengthFieldLength is only set for H264/H265.
+ public TrackOutput output;
+ public int nalUnitLengthFieldLength;
+
+ /** Initializes the track with an output. */
+ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserException {
+ String mimeType;
+ int maxInputSize = Format.NO_VALUE;
+ @C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
+ List<byte[]> initializationData = null;
+ switch (codecId) {
+ case CODEC_ID_VP8:
+ mimeType = MimeTypes.VIDEO_VP8;
+ break;
+ case CODEC_ID_VP9:
+ mimeType = MimeTypes.VIDEO_VP9;
+ break;
+ case CODEC_ID_AV1:
+ mimeType = MimeTypes.VIDEO_AV1;
+ break;
+ case CODEC_ID_MPEG2:
+ mimeType = MimeTypes.VIDEO_MPEG2;
+ break;
+ case CODEC_ID_MPEG4_SP:
+ case CODEC_ID_MPEG4_ASP:
+ case CODEC_ID_MPEG4_AP:
+ mimeType = MimeTypes.VIDEO_MP4V;
+ initializationData =
+ codecPrivate == null ? null : Collections.singletonList(codecPrivate);
+ break;
+ case CODEC_ID_H264:
+ mimeType = MimeTypes.VIDEO_H264;
+ AvcConfig avcConfig = AvcConfig.parse(new ParsableByteArray(codecPrivate));
+ initializationData = avcConfig.initializationData;
+ nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;
+ break;
+ case CODEC_ID_H265:
+ mimeType = MimeTypes.VIDEO_H265;
+ HevcConfig hevcConfig = HevcConfig.parse(new ParsableByteArray(codecPrivate));
+ initializationData = hevcConfig.initializationData;
+ nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength;
+ break;
+ case CODEC_ID_FOURCC:
+ Pair<String, List<byte[]>> pair = parseFourCcPrivate(new ParsableByteArray(codecPrivate));
+ mimeType = pair.first;
+ initializationData = pair.second;
+ break;
+ case CODEC_ID_THEORA:
+ // TODO: This can be set to the real mimeType if/when we work out what initializationData
+ // should be set to for this case.
+ mimeType = MimeTypes.VIDEO_UNKNOWN;
+ break;
+ case CODEC_ID_VORBIS:
+ mimeType = MimeTypes.AUDIO_VORBIS;
+ maxInputSize = VORBIS_MAX_INPUT_SIZE;
+ initializationData = parseVorbisCodecPrivate(codecPrivate);
+ break;
+ case CODEC_ID_OPUS:
+ mimeType = MimeTypes.AUDIO_OPUS;
+ maxInputSize = OPUS_MAX_INPUT_SIZE;
+ initializationData = new ArrayList<>(3);
+ initializationData.add(codecPrivate);
+ initializationData.add(
+ ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(codecDelayNs).array());
+ initializationData.add(
+ ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(seekPreRollNs).array());
+ break;
+ case CODEC_ID_AAC:
+ mimeType = MimeTypes.AUDIO_AAC;
+ initializationData = Collections.singletonList(codecPrivate);
+ break;
+ case CODEC_ID_MP2:
+ mimeType = MimeTypes.AUDIO_MPEG_L2;
+ maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
+ break;
+ case CODEC_ID_MP3:
+ mimeType = MimeTypes.AUDIO_MPEG;
+ maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
+ break;
+ case CODEC_ID_AC3:
+ mimeType = MimeTypes.AUDIO_AC3;
+ break;
+ case CODEC_ID_E_AC3:
+ mimeType = MimeTypes.AUDIO_E_AC3;
+ break;
+ case CODEC_ID_TRUEHD:
+ mimeType = MimeTypes.AUDIO_TRUEHD;
+ trueHdSampleRechunker = new TrueHdSampleRechunker();
+ break;
+ case CODEC_ID_DTS:
+ case CODEC_ID_DTS_EXPRESS:
+ mimeType = MimeTypes.AUDIO_DTS;
+ break;
+ case CODEC_ID_DTS_LOSSLESS:
+ mimeType = MimeTypes.AUDIO_DTS_HD;
+ break;
+ case CODEC_ID_FLAC:
+ mimeType = MimeTypes.AUDIO_FLAC;
+ initializationData = Collections.singletonList(codecPrivate);
+ break;
+ case CODEC_ID_ACM:
+ mimeType = MimeTypes.AUDIO_RAW;
+ if (parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) {
+ pcmEncoding = Util.getPcmEncoding(audioBitDepth);
+ if (pcmEncoding == C.ENCODING_INVALID) {
+ pcmEncoding = Format.NO_VALUE;
+ mimeType = MimeTypes.AUDIO_UNKNOWN;
+ Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to "
+ + mimeType);
+ }
+ } else {
+ mimeType = MimeTypes.AUDIO_UNKNOWN;
+ Log.w(TAG, "Non-PCM MS/ACM is unsupported. Setting mimeType to " + mimeType);
+ }
+ break;
+ case CODEC_ID_PCM_INT_LIT:
+ mimeType = MimeTypes.AUDIO_RAW;
+ pcmEncoding = Util.getPcmEncoding(audioBitDepth);
+ if (pcmEncoding == C.ENCODING_INVALID) {
+ pcmEncoding = Format.NO_VALUE;
+ mimeType = MimeTypes.AUDIO_UNKNOWN;
+ Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to "
+ + mimeType);
+ }
+ break;
+ case CODEC_ID_SUBRIP:
+ mimeType = MimeTypes.APPLICATION_SUBRIP;
+ break;
+ case CODEC_ID_ASS:
+ mimeType = MimeTypes.TEXT_SSA;
+ break;
+ case CODEC_ID_VOBSUB:
+ mimeType = MimeTypes.APPLICATION_VOBSUB;
+ initializationData = Collections.singletonList(codecPrivate);
+ break;
+ case CODEC_ID_PGS:
+ mimeType = MimeTypes.APPLICATION_PGS;
+ break;
+ case CODEC_ID_DVBSUB:
+ mimeType = MimeTypes.APPLICATION_DVBSUBS;
+ // Init data: composition_page (2), ancillary_page (2)
+ initializationData = Collections.singletonList(new byte[] {codecPrivate[0],
+ codecPrivate[1], codecPrivate[2], codecPrivate[3]});
+ break;
+ default:
+ throw new ParserException("Unrecognized codec identifier.");
+ }
+
+ int type;
+ Format format;
+ @C.SelectionFlags int selectionFlags = 0;
+ selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0;
+ selectionFlags |= flagForced ? C.SELECTION_FLAG_FORCED : 0;
+ // TODO: Consider reading the name elements of the tracks and, if present, incorporating them
+ // into the trackId passed when creating the formats.
+ if (MimeTypes.isAudio(mimeType)) {
+ type = C.TRACK_TYPE_AUDIO;
+ format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding,
+ initializationData, drmInitData, selectionFlags, language);
+ } else if (MimeTypes.isVideo(mimeType)) {
+ type = C.TRACK_TYPE_VIDEO;
+ if (displayUnit == Track.DISPLAY_UNIT_PIXELS) {
+ displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth;
+ displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight;
+ }
+ float pixelWidthHeightRatio = Format.NO_VALUE;
+ if (displayWidth != Format.NO_VALUE && displayHeight != Format.NO_VALUE) {
+ pixelWidthHeightRatio = ((float) (height * displayWidth)) / (width * displayHeight);
+ }
+ ColorInfo colorInfo = null;
+ if (hasColorInfo) {
+ byte[] hdrStaticInfo = getHdrStaticInfo();
+ colorInfo = new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo);
+ }
+ int rotationDegrees = Format.NO_VALUE;
+ // Some HTC devices signal rotation in track names.
+ if ("htc_video_rotA-000".equals(name)) {
+ rotationDegrees = 0;
+ } else if ("htc_video_rotA-090".equals(name)) {
+ rotationDegrees = 90;
+ } else if ("htc_video_rotA-180".equals(name)) {
+ rotationDegrees = 180;
+ } else if ("htc_video_rotA-270".equals(name)) {
+ rotationDegrees = 270;
+ }
+ if (projectionType == C.PROJECTION_RECTANGULAR
+ && Float.compare(projectionPoseYaw, 0f) == 0
+ && Float.compare(projectionPosePitch, 0f) == 0) {
+ // The range of projectionPoseRoll is [-180, 180].
+ if (Float.compare(projectionPoseRoll, 0f) == 0) {
+ rotationDegrees = 0;
+ } else if (Float.compare(projectionPosePitch, 90f) == 0) {
+ rotationDegrees = 90;
+ } else if (Float.compare(projectionPosePitch, -180f) == 0
+ || Float.compare(projectionPosePitch, 180f) == 0) {
+ rotationDegrees = 180;
+ } else if (Float.compare(projectionPosePitch, -90f) == 0) {
+ rotationDegrees = 270;
+ }
+ }
+ format =
+ Format.createVideoSampleFormat(
+ Integer.toString(trackId),
+ mimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ maxInputSize,
+ width,
+ height,
+ /* frameRate= */ Format.NO_VALUE,
+ initializationData,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ drmInitData);
+ } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) {
+ type = C.TRACK_TYPE_TEXT;
+ format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, selectionFlags,
+ language, drmInitData);
+ } else if (MimeTypes.TEXT_SSA.equals(mimeType)) {
+ type = C.TRACK_TYPE_TEXT;
+ initializationData = new ArrayList<>(2);
+ initializationData.add(SSA_DIALOGUE_FORMAT);
+ initializationData.add(codecPrivate);
+ format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, selectionFlags, language, Format.NO_VALUE, drmInitData,
+ Format.OFFSET_SAMPLE_RELATIVE, initializationData);
+ } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType)
+ || MimeTypes.APPLICATION_PGS.equals(mimeType)
+ || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) {
+ type = C.TRACK_TYPE_TEXT;
+ format =
+ Format.createImageSampleFormat(
+ Integer.toString(trackId),
+ mimeType,
+ null,
+ Format.NO_VALUE,
+ selectionFlags,
+ initializationData,
+ language,
+ drmInitData);
+ } else {
+ throw new ParserException("Unexpected MIME type.");
+ }
+
+ this.output = output.track(number, type);
+ this.output.format(format);
+ }
+
+ /** Forces any pending sample metadata to be flushed to the output. */
+ public void outputPendingSampleMetadata() {
+ if (trueHdSampleRechunker != null) {
+ trueHdSampleRechunker.outputPendingSampleMetadata(this);
+ }
+ }
+
+ /** Resets any state stored in the track in response to a seek. */
+ public void reset() {
+ if (trueHdSampleRechunker != null) {
+ trueHdSampleRechunker.reset();
+ }
+ }
+
+ /** Returns the HDR Static Info as defined in CTA-861.3. */
+ @Nullable
+ private byte[] getHdrStaticInfo() {
+ // Are all fields present.
+ if (primaryRChromaticityX == Format.NO_VALUE || primaryRChromaticityY == Format.NO_VALUE
+ || primaryGChromaticityX == Format.NO_VALUE || primaryGChromaticityY == Format.NO_VALUE
+ || primaryBChromaticityX == Format.NO_VALUE || primaryBChromaticityY == Format.NO_VALUE
+ || whitePointChromaticityX == Format.NO_VALUE
+ || whitePointChromaticityY == Format.NO_VALUE || maxMasteringLuminance == Format.NO_VALUE
+ || minMasteringLuminance == Format.NO_VALUE) {
+ return null;
+ }
+
+ byte[] hdrStaticInfoData = new byte[25];
+ ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData).order(ByteOrder.LITTLE_ENDIAN);
+ hdrStaticInfo.put((byte) 0); // Type.
+ hdrStaticInfo.putShort((short) ((primaryRChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryRChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryGChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryGChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryBChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryBChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((whitePointChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((whitePointChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) (maxMasteringLuminance + 0.5f));
+ hdrStaticInfo.putShort((short) (minMasteringLuminance + 0.5f));
+ hdrStaticInfo.putShort((short) maxContentLuminance);
+ hdrStaticInfo.putShort((short) maxFrameAverageLuminance);
+ return hdrStaticInfoData;
+ }
+
+ /**
+ * Builds initialization data for a {@link Format} from FourCC codec private data.
+ *
+ * @return The codec mime type and initialization data. If the compression type is not supported
+ * then the mime type is set to {@link MimeTypes#VIDEO_UNKNOWN} and the initialization data
+ * is {@code null}.
+ * @throws ParserException If the initialization data could not be built.
+ */
+ private static Pair<String, List<byte[]>> parseFourCcPrivate(ParsableByteArray buffer)
+ throws ParserException {
+ try {
+ buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2).
+ long compression = buffer.readLittleEndianUnsignedInt();
+ if (compression == FOURCC_COMPRESSION_DIVX) {
+ return new Pair<>(MimeTypes.VIDEO_DIVX, null);
+ } else if (compression == FOURCC_COMPRESSION_H263) {
+ return new Pair<>(MimeTypes.VIDEO_H263, null);
+ } else if (compression == FOURCC_COMPRESSION_VC1) {
+ // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20
+ // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4).
+ int startOffset = buffer.getPosition() + 20;
+ byte[] bufferData = buffer.data;
+ for (int offset = startOffset; offset < bufferData.length - 4; offset++) {
+ if (bufferData[offset] == 0x00
+ && bufferData[offset + 1] == 0x00
+ && bufferData[offset + 2] == 0x01
+ && bufferData[offset + 3] == 0x0F) {
+ // We've found the initialization data.
+ byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length);
+ return new Pair<>(MimeTypes.VIDEO_VC1, Collections.singletonList(initializationData));
+ }
+ }
+ throw new ParserException("Failed to find FourCC VC1 initialization data");
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParserException("Error parsing FourCC private data");
+ }
+
+ Log.w(TAG, "Unknown FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN);
+ return new Pair<>(MimeTypes.VIDEO_UNKNOWN, null);
+ }
+
+ /**
+ * Builds initialization data for a {@link Format} from Vorbis codec private data.
+ *
+ * @return The initialization data for the {@link Format}.
+ * @throws ParserException If the initialization data could not be built.
+ */
+ private static List<byte[]> parseVorbisCodecPrivate(byte[] codecPrivate)
+ throws ParserException {
+ try {
+ if (codecPrivate[0] != 0x02) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ int offset = 1;
+ int vorbisInfoLength = 0;
+ while (codecPrivate[offset] == (byte) 0xFF) {
+ vorbisInfoLength += 0xFF;
+ offset++;
+ }
+ vorbisInfoLength += codecPrivate[offset++];
+
+ int vorbisSkipLength = 0;
+ while (codecPrivate[offset] == (byte) 0xFF) {
+ vorbisSkipLength += 0xFF;
+ offset++;
+ }
+ vorbisSkipLength += codecPrivate[offset++];
+
+ if (codecPrivate[offset] != 0x01) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ byte[] vorbisInfo = new byte[vorbisInfoLength];
+ System.arraycopy(codecPrivate, offset, vorbisInfo, 0, vorbisInfoLength);
+ offset += vorbisInfoLength;
+ if (codecPrivate[offset] != 0x03) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ offset += vorbisSkipLength;
+ if (codecPrivate[offset] != 0x05) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ byte[] vorbisBooks = new byte[codecPrivate.length - offset];
+ System.arraycopy(codecPrivate, offset, vorbisBooks, 0, codecPrivate.length - offset);
+ List<byte[]> initializationData = new ArrayList<>(2);
+ initializationData.add(vorbisInfo);
+ initializationData.add(vorbisBooks);
+ return initializationData;
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ }
+
+ /**
+ * Parses an MS/ACM codec private, returning whether it indicates PCM audio.
+ *
+ * @return Whether the codec private indicates PCM audio.
+ * @throws ParserException If a parsing error occurs.
+ */
+ private static boolean parseMsAcmCodecPrivate(ParsableByteArray buffer) throws ParserException {
+ try {
+ int formatTag = buffer.readLittleEndianUnsignedShort();
+ if (formatTag == WAVE_FORMAT_PCM) {
+ return true;
+ } else if (formatTag == WAVE_FORMAT_EXTENSIBLE) {
+ buffer.setPosition(WAVE_FORMAT_SIZE + 6); // unionSamples(2), channelMask(4)
+ return buffer.readLong() == WAVE_SUBFORMAT_PCM.getMostSignificantBits()
+ && buffer.readLong() == WAVE_SUBFORMAT_PCM.getLeastSignificantBits();
+ } else {
+ return false;
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParserException("Error parsing MS/ACM codec private");
+ }
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java
new file mode 100644
index 0000000000..f84cd084a3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java
@@ -0,0 +1,114 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * Utility class that peeks from the input stream in order to determine whether it appears to be
+ * compatible input for this extractor.
+ */
+/* package */ final class Sniffer {
+
+ /**
+ * The number of bytes to search for a valid header in {@link #sniff(ExtractorInput)}.
+ */
+ private static final int SEARCH_LENGTH = 1024;
+ private static final int ID_EBML = 0x1A45DFA3;
+
+ private final ParsableByteArray scratch;
+ private int peekLength;
+
+ public Sniffer() {
+ scratch = new ParsableByteArray(8);
+ }
+
+ /**
+ * @see com.google.android.exoplayer2.extractor.Extractor#sniff(ExtractorInput)
+ */
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ long inputLength = input.getLength();
+ int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH
+ ? SEARCH_LENGTH : inputLength);
+ // Find four bytes equal to ID_EBML near the start of the input.
+ input.peekFully(scratch.data, 0, 4);
+ long tag = scratch.readUnsignedInt();
+ peekLength = 4;
+ while (tag != ID_EBML) {
+ if (++peekLength == bytesToSearch) {
+ return false;
+ }
+ input.peekFully(scratch.data, 0, 1);
+ tag = (tag << 8) & 0xFFFFFF00;
+ tag |= scratch.data[0] & 0xFF;
+ }
+
+ // Read the size of the EBML header and make sure it is within the stream.
+ long headerSize = readUint(input);
+ long headerStart = peekLength;
+ if (headerSize == Long.MIN_VALUE
+ || (inputLength != C.LENGTH_UNSET && headerStart + headerSize >= inputLength)) {
+ return false;
+ }
+
+ // Read the payload elements in the EBML header.
+ while (peekLength < headerStart + headerSize) {
+ long id = readUint(input);
+ if (id == Long.MIN_VALUE) {
+ return false;
+ }
+ long size = readUint(input);
+ if (size < 0 || size > Integer.MAX_VALUE) {
+ return false;
+ }
+ if (size != 0) {
+ int sizeInt = (int) size;
+ input.advancePeekPosition(sizeInt);
+ peekLength += sizeInt;
+ }
+ }
+ return peekLength == headerStart + headerSize;
+ }
+
+ /**
+ * Peeks a variable-length unsigned EBML integer from the input.
+ */
+ private long readUint(ExtractorInput input) throws IOException, InterruptedException {
+ input.peekFully(scratch.data, 0, 1);
+ int value = scratch.data[0] & 0xFF;
+ if (value == 0) {
+ return Long.MIN_VALUE;
+ }
+ int mask = 0x80;
+ int length = 0;
+ while ((value & mask) == 0) {
+ mask >>= 1;
+ length++;
+ }
+ value &= ~mask;
+ input.peekFully(scratch.data, 1, length);
+ for (int i = 0; i < length; i++) {
+ value <<= 8;
+ value += scratch.data[i + 1] & 0xFF;
+ }
+ peekLength += length + 1;
+ return value;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/VarintReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/VarintReader.java
new file mode 100644
index 0000000000..8a8d572ea5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/VarintReader.java
@@ -0,0 +1,155 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Reads EBML variable-length integers (varints) from an {@link ExtractorInput}.
+ */
+/* package */ final class VarintReader {
+
+ private static final int STATE_BEGIN_READING = 0;
+ private static final int STATE_READ_CONTENTS = 1;
+
+ /**
+ * The first byte of a variable-length integer (varint) will have one of these bit masks
+ * indicating the total length in bytes.
+ *
+ * <p>{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes.
+ */
+ private static final long[] VARINT_LENGTH_MASKS = new long[] {
+ 0x80L, 0x40L, 0x20L, 0x10L, 0x08L, 0x04L, 0x02L, 0x01L
+ };
+
+ private final byte[] scratch;
+
+ private int state;
+ private int length;
+
+ public VarintReader() {
+ scratch = new byte[8];
+ }
+
+ /**
+ * Resets the reader to start reading a new variable-length integer.
+ */
+ public void reset() {
+ state = STATE_BEGIN_READING;
+ length = 0;
+ }
+
+ /**
+ * Reads an EBML variable-length integer (varint) from an {@link ExtractorInput} such that
+ * reading can be resumed later if an error occurs having read only some of it.
+ * <p>
+ * If an value is successfully read, then the reader will automatically reset itself ready to
+ * read another value.
+ * <p>
+ * If an {@link IOException} or {@link InterruptedException} is throw, the read can be resumed
+ * later by calling this method again, passing an {@link ExtractorInput} providing data starting
+ * where the previous one left off.
+ *
+ * @param input The {@link ExtractorInput} from which the integer should be read.
+ * @param allowEndOfInput True if encountering the end of the input having read no data is
+ * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it
+ * should be considered an error, causing an {@link EOFException} to be thrown.
+ * @param removeLengthMask Removes the variable-length integer length mask from the value.
+ * @param maximumAllowedLength Maximum allowed length of the variable integer to be read.
+ * @return The read value, or {@link C#RESULT_END_OF_INPUT} if {@code allowEndOfStream} is true
+ * and the end of the input was encountered, or {@link C#RESULT_MAX_LENGTH_EXCEEDED} if the
+ * length of the varint exceeded maximumAllowedLength.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public long readUnsignedVarint(ExtractorInput input, boolean allowEndOfInput,
+ boolean removeLengthMask, int maximumAllowedLength) throws IOException, InterruptedException {
+ if (state == STATE_BEGIN_READING) {
+ // Read the first byte to establish the length.
+ if (!input.readFully(scratch, 0, 1, allowEndOfInput)) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ int firstByte = scratch[0] & 0xFF;
+ length = parseUnsignedVarintLength(firstByte);
+ if (length == C.LENGTH_UNSET) {
+ throw new IllegalStateException("No valid varint length mask found");
+ }
+ state = STATE_READ_CONTENTS;
+ }
+
+ if (length > maximumAllowedLength) {
+ state = STATE_BEGIN_READING;
+ return C.RESULT_MAX_LENGTH_EXCEEDED;
+ }
+
+ if (length != 1) {
+ // Read the remaining bytes.
+ input.readFully(scratch, 1, length - 1);
+ }
+
+ state = STATE_BEGIN_READING;
+ return assembleVarint(scratch, length, removeLengthMask);
+ }
+
+ /**
+ * Returns the number of bytes occupied by the most recently parsed varint.
+ */
+ public int getLastLength() {
+ return length;
+ }
+
+ /**
+ * Parses and the length of the varint given the first byte.
+ *
+ * @param firstByte First byte of the varint.
+ * @return Length of the varint beginning with the given byte if it was valid,
+ * {@link C#LENGTH_UNSET} otherwise.
+ */
+ public static int parseUnsignedVarintLength(int firstByte) {
+ int varIntLength = C.LENGTH_UNSET;
+ for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
+ if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
+ varIntLength = i + 1;
+ break;
+ }
+ }
+ return varIntLength;
+ }
+
+ /**
+ * Assemble a varint from the given byte array.
+ *
+ * @param varintBytes Bytes that make up the varint.
+ * @param varintLength Length of the varint to assemble.
+ * @param removeLengthMask Removes the variable-length integer length mask from the value.
+ * @return Parsed and assembled varint.
+ */
+ public static long assembleVarint(byte[] varintBytes, int varintLength,
+ boolean removeLengthMask) {
+ long varint = varintBytes[0] & 0xFFL;
+ if (removeLengthMask) {
+ varint &= ~VARINT_LENGTH_MASKS[varintLength - 1];
+ }
+ for (int i = 1; i < varintLength; i++) {
+ varint = (varint << 8) | (varintBytes[i] & 0xFFL);
+ }
+ return varint;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
new file mode 100644
index 0000000000..1a442110e3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
@@ -0,0 +1,46 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader;
+
+/**
+ * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
+ */
+/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap implements Seeker {
+
+ /**
+ * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
+ * @param firstFramePosition The position of the first frame in the stream.
+ * @param mpegAudioHeader The MPEG audio header associated with the first frame.
+ */
+ public ConstantBitrateSeeker(
+ long inputLength, long firstFramePosition, MpegAudioHeader mpegAudioHeader) {
+ super(inputLength, firstFramePosition, mpegAudioHeader.bitrate, mpegAudioHeader.frameSize);
+ }
+
+ @Override
+ public long getTimeUs(long position) {
+ return getTimeUsAtPosition(position);
+ }
+
+ @Override
+ public long getDataEndPosition() {
+ return C.POSITION_UNSET;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java
new file mode 100644
index 0000000000..662ded4ec3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3;
+
+import android.util.Pair;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.MlltFrame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/** MP3 seeker that uses metadata from an {@link MlltFrame}. */
+/* package */ final class MlltSeeker implements Seeker {
+
+ /**
+ * Returns an {@link MlltSeeker} for seeking in the stream.
+ *
+ * @param firstFramePosition The position of the start of the first frame in the stream.
+ * @param mlltFrame The MLLT frame with seeking metadata.
+ * @return An {@link MlltSeeker} for seeking in the stream.
+ */
+ public static MlltSeeker create(long firstFramePosition, MlltFrame mlltFrame) {
+ int referenceCount = mlltFrame.bytesDeviations.length;
+ long[] referencePositions = new long[1 + referenceCount];
+ long[] referenceTimesMs = new long[1 + referenceCount];
+ referencePositions[0] = firstFramePosition;
+ referenceTimesMs[0] = 0;
+ long position = firstFramePosition;
+ long timeMs = 0;
+ for (int i = 1; i <= referenceCount; i++) {
+ position += mlltFrame.bytesBetweenReference + mlltFrame.bytesDeviations[i - 1];
+ timeMs += mlltFrame.millisecondsBetweenReference + mlltFrame.millisecondsDeviations[i - 1];
+ referencePositions[i] = position;
+ referenceTimesMs[i] = timeMs;
+ }
+ return new MlltSeeker(referencePositions, referenceTimesMs);
+ }
+
+ private final long[] referencePositions;
+ private final long[] referenceTimesMs;
+ private final long durationUs;
+
+ private MlltSeeker(long[] referencePositions, long[] referenceTimesMs) {
+ this.referencePositions = referencePositions;
+ this.referenceTimesMs = referenceTimesMs;
+ // Use the last reference point as the duration, as extrapolating variable bitrate at the end of
+ // the stream may give a large error.
+ durationUs = C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]);
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ timeUs = Util.constrainValue(timeUs, 0, durationUs);
+ Pair<Long, Long> timeMsAndPosition =
+ linearlyInterpolate(C.usToMs(timeUs), referenceTimesMs, referencePositions);
+ timeUs = C.msToUs(timeMsAndPosition.first);
+ long position = timeMsAndPosition.second;
+ return new SeekPoints(new SeekPoint(timeUs, position));
+ }
+
+ @Override
+ public long getTimeUs(long position) {
+ Pair<Long, Long> positionAndTimeMs =
+ linearlyInterpolate(position, referencePositions, referenceTimesMs);
+ return C.msToUs(positionAndTimeMs.second);
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /**
+ * Given a set of reference points as coordinates in {@code xReferences} and {@code yReferences}
+ * and an x-axis value, linearly interpolates between corresponding reference points to give a
+ * y-axis value.
+ *
+ * @param x The x-axis value for which a y-axis value is needed.
+ * @param xReferences x coordinates of reference points.
+ * @param yReferences y coordinates of reference points.
+ * @return The linearly interpolated y-axis value.
+ */
+ private static Pair<Long, Long> linearlyInterpolate(
+ long x, long[] xReferences, long[] yReferences) {
+ int previousReferenceIndex =
+ Util.binarySearchFloor(xReferences, x, /* inclusive= */ true, /* stayInBounds= */ true);
+ long xPreviousReference = xReferences[previousReferenceIndex];
+ long yPreviousReference = yReferences[previousReferenceIndex];
+ int nextReferenceIndex = previousReferenceIndex + 1;
+ if (nextReferenceIndex == xReferences.length) {
+ return Pair.create(xPreviousReference, yPreviousReference);
+ } else {
+ long xNextReference = xReferences[nextReferenceIndex];
+ long yNextReference = yReferences[nextReferenceIndex];
+ double proportion =
+ xNextReference == xPreviousReference
+ ? 0.0
+ : ((double) x - xPreviousReference) / (xNextReference - xPreviousReference);
+ long y = (long) (proportion * (yNextReference - yPreviousReference)) + yPreviousReference;
+ return Pair.create(x, y);
+ }
+ }
+
+ @Override
+ public long getDataEndPosition() {
+ return C.POSITION_UNSET;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
new file mode 100644
index 0000000000..2829a1e519
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
@@ -0,0 +1,482 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Id3Peeker;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3.Seeker.UnseekableSeeker;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.MlltFrame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Extracts data from the MP3 container format.
+ */
+public final class Mp3Extractor implements Extractor {
+
+ /** Factory for {@link Mp3Extractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp3Extractor()};
+
+ /**
+ * Flags controlling the behavior of the extractor. Possible flag values are {@link
+ * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} and {@link #FLAG_DISABLE_ID3_METADATA}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA})
+ public @interface Flags {}
+ /**
+ * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would
+ * otherwise not be possible.
+ */
+ public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
+ /**
+ * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
+ * required.
+ */
+ public static final int FLAG_DISABLE_ID3_METADATA = 2;
+
+ /** Predicate that matches ID3 frames containing only required gapless/seeking metadata. */
+ private static final FramePredicate REQUIRED_ID3_FRAME_PREDICATE =
+ (majorVersion, id0, id1, id2, id3) ->
+ ((id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2))
+ || (id0 == 'M' && id1 == 'L' && id2 == 'L' && (id3 == 'T' || majorVersion == 2)));
+
+ /**
+ * The maximum number of bytes to search when synchronizing, before giving up.
+ */
+ private static final int MAX_SYNC_BYTES = 128 * 1024;
+ /**
+ * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.
+ */
+ private static final int MAX_SNIFF_BYTES = 16 * 1024;
+ /**
+ * Maximum length of data read into {@link #scratch}.
+ */
+ private static final int SCRATCH_LENGTH = 10;
+
+ /**
+ * Mask that includes the audio header values that must match between frames.
+ */
+ private static final int MPEG_AUDIO_HEADER_MASK = 0xFFFE0C00;
+
+ private static final int SEEK_HEADER_XING = 0x58696e67;
+ private static final int SEEK_HEADER_INFO = 0x496e666f;
+ private static final int SEEK_HEADER_VBRI = 0x56425249;
+ private static final int SEEK_HEADER_UNSET = 0;
+
+ @Flags private final int flags;
+ private final long forcedFirstSampleTimestampUs;
+ private final ParsableByteArray scratch;
+ private final MpegAudioHeader synchronizedHeader;
+ private final GaplessInfoHolder gaplessInfoHolder;
+ private final Id3Peeker id3Peeker;
+
+ // Extractor outputs.
+ private ExtractorOutput extractorOutput;
+ private TrackOutput trackOutput;
+
+ private int synchronizedHeaderData;
+
+ private Metadata metadata;
+ @Nullable private Seeker seeker;
+ private boolean disableSeeking;
+ private long basisTimeUs;
+ private long samplesRead;
+ private long firstSamplePosition;
+ private int sampleBytesRemaining;
+
+ public Mp3Extractor() {
+ this(0);
+ }
+
+ /**
+ * @param flags Flags that control the extractor's behavior.
+ */
+ public Mp3Extractor(@Flags int flags) {
+ this(flags, C.TIME_UNSET);
+ }
+
+ /**
+ * @param flags Flags that control the extractor's behavior.
+ * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or
+ * {@link C#TIME_UNSET} if forcing is not required.
+ */
+ public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) {
+ this.flags = flags;
+ this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
+ scratch = new ParsableByteArray(SCRATCH_LENGTH);
+ synchronizedHeader = new MpegAudioHeader();
+ gaplessInfoHolder = new GaplessInfoHolder();
+ basisTimeUs = C.TIME_UNSET;
+ id3Peeker = new Id3Peeker();
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return synchronize(input, true);
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
+ extractorOutput.endTracks();
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ synchronizedHeaderData = 0;
+ basisTimeUs = C.TIME_UNSET;
+ samplesRead = 0;
+ sampleBytesRemaining = 0;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ if (synchronizedHeaderData == 0) {
+ try {
+ synchronize(input, false);
+ } catch (EOFException e) {
+ return RESULT_END_OF_INPUT;
+ }
+ }
+ if (seeker == null) {
+ // Read past any seek frame and set the seeker based on metadata or a seek frame. Metadata
+ // takes priority as it can provide greater precision.
+ Seeker seekFrameSeeker = maybeReadSeekFrame(input);
+ Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition());
+
+ if (disableSeeking) {
+ seeker = new UnseekableSeeker();
+ } else {
+ if (metadataSeeker != null) {
+ seeker = metadataSeeker;
+ } else if (seekFrameSeeker != null) {
+ seeker = seekFrameSeeker;
+ }
+ if (seeker == null
+ || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
+ seeker = getConstantBitrateSeeker(input);
+ }
+ }
+ extractorOutput.seekMap(seeker);
+ trackOutput.format(
+ Format.createAudioSampleFormat(
+ /* id= */ null,
+ synchronizedHeader.mimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ MpegAudioHeader.MAX_FRAME_SIZE_BYTES,
+ synchronizedHeader.channels,
+ synchronizedHeader.sampleRate,
+ /* pcmEncoding= */ Format.NO_VALUE,
+ gaplessInfoHolder.encoderDelay,
+ gaplessInfoHolder.encoderPadding,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null,
+ (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));
+ firstSamplePosition = input.getPosition();
+ } else if (firstSamplePosition != 0) {
+ long inputPosition = input.getPosition();
+ if (inputPosition < firstSamplePosition) {
+ // Skip past the seek frame.
+ input.skipFully((int) (firstSamplePosition - inputPosition));
+ }
+ }
+ return readSample(input);
+ }
+
+ /**
+ * Disables the extractor from being able to seek through the media.
+ *
+ * <p>Please note that this needs to be called before {@link #read}.
+ */
+ public void disableSeeking() {
+ disableSeeking = true;
+ }
+
+ // Internal methods.
+
+ private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
+ if (sampleBytesRemaining == 0) {
+ extractorInput.resetPeekPosition();
+ if (peekEndOfStreamOrHeader(extractorInput)) {
+ return RESULT_END_OF_INPUT;
+ }
+ scratch.setPosition(0);
+ int sampleHeaderData = scratch.readInt();
+ if (!headersMatch(sampleHeaderData, synchronizedHeaderData)
+ || MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) {
+ // We have lost synchronization, so attempt to resynchronize starting at the next byte.
+ extractorInput.skipFully(1);
+ synchronizedHeaderData = 0;
+ return RESULT_CONTINUE;
+ }
+ MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader);
+ if (basisTimeUs == C.TIME_UNSET) {
+ basisTimeUs = seeker.getTimeUs(extractorInput.getPosition());
+ if (forcedFirstSampleTimestampUs != C.TIME_UNSET) {
+ long embeddedFirstSampleTimestampUs = seeker.getTimeUs(0);
+ basisTimeUs += forcedFirstSampleTimestampUs - embeddedFirstSampleTimestampUs;
+ }
+ }
+ sampleBytesRemaining = synchronizedHeader.frameSize;
+ }
+ int bytesAppended = trackOutput.sampleData(extractorInput, sampleBytesRemaining, true);
+ if (bytesAppended == C.RESULT_END_OF_INPUT) {
+ return RESULT_END_OF_INPUT;
+ }
+ sampleBytesRemaining -= bytesAppended;
+ if (sampleBytesRemaining > 0) {
+ return RESULT_CONTINUE;
+ }
+ long timeUs = basisTimeUs + (samplesRead * C.MICROS_PER_SECOND / synchronizedHeader.sampleRate);
+ trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, synchronizedHeader.frameSize, 0,
+ null);
+ samplesRead += synchronizedHeader.samplesPerFrame;
+ sampleBytesRemaining = 0;
+ return RESULT_CONTINUE;
+ }
+
+ private boolean synchronize(ExtractorInput input, boolean sniffing)
+ throws IOException, InterruptedException {
+ int validFrameCount = 0;
+ int candidateSynchronizedHeaderData = 0;
+ int peekedId3Bytes = 0;
+ int searchedBytes = 0;
+ int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
+ input.resetPeekPosition();
+ if (input.getPosition() == 0) {
+ // We need to parse enough ID3 metadata to retrieve any gapless/seeking playback information
+ // even if ID3 metadata parsing is disabled.
+ boolean parseAllId3Frames = (flags & FLAG_DISABLE_ID3_METADATA) == 0;
+ Id3Decoder.FramePredicate id3FramePredicate =
+ parseAllId3Frames ? null : REQUIRED_ID3_FRAME_PREDICATE;
+ metadata = id3Peeker.peekId3Data(input, id3FramePredicate);
+ if (metadata != null) {
+ gaplessInfoHolder.setFromMetadata(metadata);
+ }
+ peekedId3Bytes = (int) input.getPeekPosition();
+ if (!sniffing) {
+ input.skipFully(peekedId3Bytes);
+ }
+ }
+ while (true) {
+ if (peekEndOfStreamOrHeader(input)) {
+ if (validFrameCount > 0) {
+ // We reached the end of the stream but found at least one valid frame.
+ break;
+ }
+ throw new EOFException();
+ }
+ scratch.setPosition(0);
+ int headerData = scratch.readInt();
+ int frameSize;
+ if ((candidateSynchronizedHeaderData != 0
+ && !headersMatch(headerData, candidateSynchronizedHeaderData))
+ || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) {
+ // The header doesn't match the candidate header or is invalid. Try the next byte offset.
+ if (searchedBytes++ == searchLimitBytes) {
+ if (!sniffing) {
+ throw new ParserException("Searched too many bytes.");
+ }
+ return false;
+ }
+ validFrameCount = 0;
+ candidateSynchronizedHeaderData = 0;
+ if (sniffing) {
+ input.resetPeekPosition();
+ input.advancePeekPosition(peekedId3Bytes + searchedBytes);
+ } else {
+ input.skipFully(1);
+ }
+ } else {
+ // The header matches the candidate header and/or is valid.
+ validFrameCount++;
+ if (validFrameCount == 1) {
+ MpegAudioHeader.populateHeader(headerData, synchronizedHeader);
+ candidateSynchronizedHeaderData = headerData;
+ } else if (validFrameCount == 4) {
+ break;
+ }
+ input.advancePeekPosition(frameSize - 4);
+ }
+ }
+ // Prepare to read the synchronized frame.
+ if (sniffing) {
+ input.skipFully(peekedId3Bytes + searchedBytes);
+ } else {
+ input.resetPeekPosition();
+ }
+ synchronizedHeaderData = candidateSynchronizedHeaderData;
+ return true;
+ }
+
+ /**
+ * Returns whether the extractor input is peeking the end of the stream. If {@code false},
+ * populates the scratch buffer with the next four bytes.
+ */
+ private boolean peekEndOfStreamOrHeader(ExtractorInput extractorInput)
+ throws IOException, InterruptedException {
+ if (seeker != null) {
+ long dataEndPosition = seeker.getDataEndPosition();
+ if (dataEndPosition != C.POSITION_UNSET
+ && extractorInput.getPeekPosition() > dataEndPosition - 4) {
+ return true;
+ }
+ }
+ try {
+ return !extractorInput.peekFully(
+ scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true);
+ } catch (EOFException e) {
+ return true;
+ }
+ }
+
+ /**
+ * Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata,
+ * returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise.
+ * After this method returns, the input position is the start of the first frame of audio.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return A {@link Seeker} if seeking metadata was present and valid, or {@code null} otherwise.
+ * @throws IOException Thrown if there was an error reading from the stream. Not expected if the
+ * next two frames were already peeked during synchronization.
+ * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if
+ * the next two frames were already peeked during synchronization.
+ */
+ private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, InterruptedException {
+ ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize);
+ input.peekFully(frame.data, 0, synchronizedHeader.frameSize);
+ int xingBase = (synchronizedHeader.version & 1) != 0
+ ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1
+ : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5
+ int seekHeader = getSeekFrameHeader(frame, xingBase);
+ Seeker seeker;
+ if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) {
+ seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame);
+ if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
+ // If there is a Xing header, read gapless playback metadata at a fixed offset.
+ input.resetPeekPosition();
+ input.advancePeekPosition(xingBase + 141);
+ input.peekFully(scratch.data, 0, 3);
+ scratch.setPosition(0);
+ gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24());
+ }
+ input.skipFully(synchronizedHeader.frameSize);
+ if (seeker != null && !seeker.isSeekable() && seekHeader == SEEK_HEADER_INFO) {
+ // Fall back to constant bitrate seeking for Info headers missing a table of contents.
+ return getConstantBitrateSeeker(input);
+ }
+ } else if (seekHeader == SEEK_HEADER_VBRI) {
+ seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame);
+ input.skipFully(synchronizedHeader.frameSize);
+ } else { // seekerHeader == SEEK_HEADER_UNSET
+ // This frame doesn't contain seeking information, so reset the peek position.
+ seeker = null;
+ input.resetPeekPosition();
+ }
+ return seeker;
+ }
+
+ /**
+ * Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate.
+ */
+ private Seeker getConstantBitrateSeeker(ExtractorInput input)
+ throws IOException, InterruptedException {
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+ MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
+ return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader);
+ }
+
+ /**
+ * Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}.
+ */
+ private static boolean headersMatch(int headerA, long headerB) {
+ return (headerA & MPEG_AUDIO_HEADER_MASK) == (headerB & MPEG_AUDIO_HEADER_MASK);
+ }
+
+ /**
+ * Returns {@link #SEEK_HEADER_XING}, {@link #SEEK_HEADER_INFO} or {@link #SEEK_HEADER_VBRI} if
+ * the provided {@code frame} may have seeking metadata, or {@link #SEEK_HEADER_UNSET} otherwise.
+ * If seeking metadata is present, {@code frame}'s position is advanced past the header.
+ */
+ private static int getSeekFrameHeader(ParsableByteArray frame, int xingBase) {
+ if (frame.limit() >= xingBase + 4) {
+ frame.setPosition(xingBase);
+ int headerData = frame.readInt();
+ if (headerData == SEEK_HEADER_XING || headerData == SEEK_HEADER_INFO) {
+ return headerData;
+ }
+ }
+ if (frame.limit() >= 40) {
+ frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes.
+ if (frame.readInt() == SEEK_HEADER_VBRI) {
+ return SEEK_HEADER_VBRI;
+ }
+ }
+ return SEEK_HEADER_UNSET;
+ }
+
+ @Nullable
+ private static MlltSeeker maybeHandleSeekMetadata(Metadata metadata, long firstFramePosition) {
+ if (metadata != null) {
+ int length = metadata.length();
+ for (int i = 0; i < length; i++) {
+ Metadata.Entry entry = metadata.get(i);
+ if (entry instanceof MlltFrame) {
+ return MlltSeeker.create(firstFramePosition, (MlltFrame) entry);
+ }
+ }
+ }
+ return null;
+ }
+
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java
new file mode 100644
index 0000000000..da0306cc60
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+
+/**
+ * {@link SeekMap} that provides the end position of audio data and also allows mapping from
+ * position (byte offset) back to time, which can be used to work out the new sample basis timestamp
+ * after seeking and resynchronization.
+ */
+/* package */ interface Seeker extends SeekMap {
+
+ /**
+ * Maps a position (byte offset) to a corresponding sample timestamp.
+ *
+ * @param position A seek position (byte offset) relative to the start of the stream.
+ * @return The corresponding timestamp of the next sample to be read, in microseconds.
+ */
+ long getTimeUs(long position);
+
+ /**
+ * Returns the position (byte offset) in the stream that is immediately after audio data, or
+ * {@link C#POSITION_UNSET} if not known.
+ */
+ long getDataEndPosition();
+
+ /** A {@link Seeker} that does not support seeking through audio data. */
+ /* package */ class UnseekableSeeker extends SeekMap.Unseekable implements Seeker {
+
+ public UnseekableSeeker() {
+ super(/* durationUs= */ C.TIME_UNSET);
+ }
+
+ @Override
+ public long getTimeUs(long position) {
+ return 0;
+ }
+
+ @Override
+ public long getDataEndPosition() {
+ // Position unset as we do not know the data end position. Note that returning 0 doesn't work.
+ return C.POSITION_UNSET;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java
new file mode 100644
index 0000000000..8bb142f496
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java
@@ -0,0 +1,136 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/** MP3 seeker that uses metadata from a VBRI header. */
+/* package */ final class VbriSeeker implements Seeker {
+
+ private static final String TAG = "VbriSeeker";
+
+ /**
+ * Returns a {@link VbriSeeker} for seeking in the stream, if required information is present.
+ * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
+ * caller should reset it.
+ *
+ * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
+ * @param position The position of the start of this frame in the stream.
+ * @param mpegAudioHeader The MPEG audio header associated with the frame.
+ * @param frame The data in this audio frame, with its position set to immediately after the
+ * 'VBRI' tag.
+ * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required
+ * information is not present.
+ */
+ public static @Nullable VbriSeeker create(
+ long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) {
+ frame.skipBytes(10);
+ int numFrames = frame.readInt();
+ if (numFrames <= 0) {
+ return null;
+ }
+ int sampleRate = mpegAudioHeader.sampleRate;
+ long durationUs = Util.scaleLargeTimestamp(numFrames,
+ C.MICROS_PER_SECOND * (sampleRate >= 32000 ? 1152 : 576), sampleRate);
+ int entryCount = frame.readUnsignedShort();
+ int scale = frame.readUnsignedShort();
+ int entrySize = frame.readUnsignedShort();
+ frame.skipBytes(2);
+
+ long minPosition = position + mpegAudioHeader.frameSize;
+ // Read table of contents entries.
+ long[] timesUs = new long[entryCount];
+ long[] positions = new long[entryCount];
+ for (int index = 0; index < entryCount; index++) {
+ timesUs[index] = (index * durationUs) / entryCount;
+ // Ensure positions do not fall within the frame containing the VBRI header. This constraint
+ // will normally only apply to the first entry in the table.
+ positions[index] = Math.max(position, minPosition);
+ int segmentSize;
+ switch (entrySize) {
+ case 1:
+ segmentSize = frame.readUnsignedByte();
+ break;
+ case 2:
+ segmentSize = frame.readUnsignedShort();
+ break;
+ case 3:
+ segmentSize = frame.readUnsignedInt24();
+ break;
+ case 4:
+ segmentSize = frame.readUnsignedIntToInt();
+ break;
+ default:
+ return null;
+ }
+ position += segmentSize * scale;
+ }
+ if (inputLength != C.LENGTH_UNSET && inputLength != position) {
+ Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position);
+ }
+ return new VbriSeeker(timesUs, positions, durationUs, /* dataEndPosition= */ position);
+ }
+
+ private final long[] timesUs;
+ private final long[] positions;
+ private final long durationUs;
+ private final long dataEndPosition;
+
+ private VbriSeeker(long[] timesUs, long[] positions, long durationUs, long dataEndPosition) {
+ this.timesUs = timesUs;
+ this.positions = positions;
+ this.durationUs = durationUs;
+ this.dataEndPosition = dataEndPosition;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ int tableIndex = Util.binarySearchFloor(timesUs, timeUs, true, true);
+ SeekPoint seekPoint = new SeekPoint(timesUs[tableIndex], positions[tableIndex]);
+ if (seekPoint.timeUs >= timeUs || tableIndex == timesUs.length - 1) {
+ return new SeekPoints(seekPoint);
+ } else {
+ SeekPoint nextSeekPoint = new SeekPoint(timesUs[tableIndex + 1], positions[tableIndex + 1]);
+ return new SeekPoints(seekPoint, nextSeekPoint);
+ }
+ }
+
+ @Override
+ public long getTimeUs(long position) {
+ return timesUs[Util.binarySearchFloor(positions, position, true, true)];
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public long getDataEndPosition() {
+ return dataEndPosition;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java
new file mode 100644
index 0000000000..61568aac93
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java
@@ -0,0 +1,188 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/** MP3 seeker that uses metadata from a Xing header. */
+/* package */ final class XingSeeker implements Seeker {
+
+ private static final String TAG = "XingSeeker";
+
+ /**
+ * Returns a {@link XingSeeker} for seeking in the stream, if required information is present.
+ * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
+ * caller should reset it.
+ *
+ * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
+ * @param position The position of the start of this frame in the stream.
+ * @param mpegAudioHeader The MPEG audio header associated with the frame.
+ * @param frame The data in this audio frame, with its position set to immediately after the
+ * 'Xing' or 'Info' tag.
+ * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required
+ * information is not present.
+ */
+ public static @Nullable XingSeeker create(
+ long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) {
+ int samplesPerFrame = mpegAudioHeader.samplesPerFrame;
+ int sampleRate = mpegAudioHeader.sampleRate;
+
+ int flags = frame.readInt();
+ int frameCount;
+ if ((flags & 0x01) != 0x01 || (frameCount = frame.readUnsignedIntToInt()) == 0) {
+ // If the frame count is missing/invalid, the header can't be used to determine the duration.
+ return null;
+ }
+ long durationUs = Util.scaleLargeTimestamp(frameCount, samplesPerFrame * C.MICROS_PER_SECOND,
+ sampleRate);
+ if ((flags & 0x06) != 0x06) {
+ // If the size in bytes or table of contents is missing, the stream is not seekable.
+ return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs);
+ }
+
+ long dataSize = frame.readUnsignedIntToInt();
+ long[] tableOfContents = new long[100];
+ for (int i = 0; i < 100; i++) {
+ tableOfContents[i] = frame.readUnsignedByte();
+ }
+
+ // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes:
+ // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4);
+ // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte();
+
+ if (inputLength != C.LENGTH_UNSET && inputLength != position + dataSize) {
+ Log.w(TAG, "XING data size mismatch: " + inputLength + ", " + (position + dataSize));
+ }
+ return new XingSeeker(
+ position, mpegAudioHeader.frameSize, durationUs, dataSize, tableOfContents);
+ }
+
+ private final long dataStartPosition;
+ private final int xingFrameSize;
+ private final long durationUs;
+ /** Data size, including the XING frame. */
+ private final long dataSize;
+
+ private final long dataEndPosition;
+ /**
+ * Entries are in the range [0, 255], but are stored as long integers for convenience. Null if the
+ * table of contents was missing from the header, in which case seeking is not be supported.
+ */
+ @Nullable private final long[] tableOfContents;
+
+ private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) {
+ this(
+ dataStartPosition,
+ xingFrameSize,
+ durationUs,
+ /* dataSize= */ C.LENGTH_UNSET,
+ /* tableOfContents= */ null);
+ }
+
+ private XingSeeker(
+ long dataStartPosition,
+ int xingFrameSize,
+ long durationUs,
+ long dataSize,
+ @Nullable long[] tableOfContents) {
+ this.dataStartPosition = dataStartPosition;
+ this.xingFrameSize = xingFrameSize;
+ this.durationUs = durationUs;
+ this.tableOfContents = tableOfContents;
+ this.dataSize = dataSize;
+ dataEndPosition = dataSize == C.LENGTH_UNSET ? C.POSITION_UNSET : dataStartPosition + dataSize;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return tableOfContents != null;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ if (!isSeekable()) {
+ return new SeekPoints(new SeekPoint(0, dataStartPosition + xingFrameSize));
+ }
+ timeUs = Util.constrainValue(timeUs, 0, durationUs);
+ double percent = (timeUs * 100d) / durationUs;
+ double scaledPosition;
+ if (percent <= 0) {
+ scaledPosition = 0;
+ } else if (percent >= 100) {
+ scaledPosition = 256;
+ } else {
+ int prevTableIndex = (int) percent;
+ long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents);
+ double prevScaledPosition = tableOfContents[prevTableIndex];
+ double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];
+ // Linearly interpolate between the two scaled positions.
+ double interpolateFraction = percent - prevTableIndex;
+ scaledPosition = prevScaledPosition
+ + (interpolateFraction * (nextScaledPosition - prevScaledPosition));
+ }
+ long positionOffset = Math.round((scaledPosition / 256) * dataSize);
+ // Ensure returned positions skip the frame containing the XING header.
+ positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1);
+ return new SeekPoints(new SeekPoint(timeUs, dataStartPosition + positionOffset));
+ }
+
+ @Override
+ public long getTimeUs(long position) {
+ long positionOffset = position - dataStartPosition;
+ if (!isSeekable() || positionOffset <= xingFrameSize) {
+ return 0L;
+ }
+ long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents);
+ double scaledPosition = (positionOffset * 256d) / dataSize;
+ int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true);
+ long prevTimeUs = getTimeUsForTableIndex(prevTableIndex);
+ long prevScaledPosition = tableOfContents[prevTableIndex];
+ long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1);
+ long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];
+ // Linearly interpolate between the two table entries.
+ double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0
+ : ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition));
+ return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs));
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public long getDataEndPosition() {
+ return dataEndPosition;
+ }
+
+ /**
+ * Returns the time in microseconds for a given table index.
+ *
+ * @param tableIndex A table index in the range [0, 100].
+ * @return The corresponding time in microseconds.
+ */
+ private long getTimeUsForTableIndex(int tableIndex) {
+ return (durationUs * tableIndex) / 100;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java
new file mode 100644
index 0000000000..56f0eab1cd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java
@@ -0,0 +1,558 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@SuppressWarnings("ConstantField")
+/* package */ abstract class Atom {
+
+ /**
+ * Size of an atom header, in bytes.
+ */
+ public static final int HEADER_SIZE = 8;
+
+ /**
+ * Size of a full atom header, in bytes.
+ */
+ public static final int FULL_HEADER_SIZE = 12;
+
+ /**
+ * Size of a long atom header, in bytes.
+ */
+ public static final int LONG_HEADER_SIZE = 16;
+
+ /**
+ * Value for the size field in an atom that defines its size in the largesize field.
+ */
+ public static final int DEFINES_LARGE_SIZE = 1;
+
+ /**
+ * Value for the size field in an atom that extends to the end of the file.
+ */
+ public static final int EXTENDS_TO_END_SIZE = 0;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_ftyp = 0x66747970;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_avc1 = 0x61766331;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_avc3 = 0x61766333;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_avcC = 0x61766343;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_hvc1 = 0x68766331;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_hev1 = 0x68657631;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_hvcC = 0x68766343;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_vp08 = 0x76703038;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_vp09 = 0x76703039;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_vpcC = 0x76706343;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_av01 = 0x61763031;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_av1C = 0x61763143;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dvav = 0x64766176;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dva1 = 0x64766131;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dvhe = 0x64766865;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dvh1 = 0x64766831;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dvcC = 0x64766343;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dvvC = 0x64767643;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_s263 = 0x73323633;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_d263 = 0x64323633;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_mdat = 0x6d646174;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_mp4a = 0x6d703461;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE__mp3 = 0x2e6d7033;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_wave = 0x77617665;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_lpcm = 0x6c70636d;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_sowt = 0x736f7774;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_ac_3 = 0x61632d33;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dac3 = 0x64616333;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_ec_3 = 0x65632d33;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dec3 = 0x64656333;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_ac_4 = 0x61632d34;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dac4 = 0x64616334;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dtsc = 0x64747363;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dtsh = 0x64747368;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dtsl = 0x6474736c;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dtse = 0x64747365;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_ddts = 0x64647473;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_tfdt = 0x74666474;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_tfhd = 0x74666864;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_trex = 0x74726578;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_trun = 0x7472756e;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_sidx = 0x73696478;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_moov = 0x6d6f6f76;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_mvhd = 0x6d766864;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_trak = 0x7472616b;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_mdia = 0x6d646961;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_minf = 0x6d696e66;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_stbl = 0x7374626c;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_esds = 0x65736473;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_moof = 0x6d6f6f66;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_traf = 0x74726166;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_mvex = 0x6d766578;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_mehd = 0x6d656864;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_tkhd = 0x746b6864;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_edts = 0x65647473;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_elst = 0x656c7374;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_mdhd = 0x6d646864;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_hdlr = 0x68646c72;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_stsd = 0x73747364;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_pssh = 0x70737368;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_sinf = 0x73696e66;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_schm = 0x7363686d;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_schi = 0x73636869;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_tenc = 0x74656e63;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_encv = 0x656e6376;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_enca = 0x656e6361;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_frma = 0x66726d61;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_saiz = 0x7361697a;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_saio = 0x7361696f;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_sbgp = 0x73626770;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_sgpd = 0x73677064;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_uuid = 0x75756964;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_senc = 0x73656e63;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_pasp = 0x70617370;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_TTML = 0x54544d4c;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_vmhd = 0x766d6864;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_mp4v = 0x6d703476;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_stts = 0x73747473;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_stss = 0x73747373;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_ctts = 0x63747473;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_stsc = 0x73747363;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_stsz = 0x7374737a;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_stz2 = 0x73747a32;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_stco = 0x7374636f;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_co64 = 0x636f3634;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_tx3g = 0x74783367;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_wvtt = 0x77767474;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_stpp = 0x73747070;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_c608 = 0x63363038;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_samr = 0x73616d72;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_sawb = 0x73617762;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_udta = 0x75647461;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_meta = 0x6d657461;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_keys = 0x6b657973;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_ilst = 0x696c7374;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_mean = 0x6d65616e;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_name = 0x6e616d65;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_data = 0x64617461;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_emsg = 0x656d7367;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_st3d = 0x73743364;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_sv3d = 0x73763364;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_proj = 0x70726f6a;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_camm = 0x63616d6d;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_alac = 0x616c6163;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_alaw = 0x616c6177;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_ulaw = 0x756c6177;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_Opus = 0x4f707573;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dOps = 0x644f7073;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_fLaC = 0x664c6143;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_dfLa = 0x64664c61;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_twos = 0x74776f73;
+
+ public final int type;
+
+ public Atom(int type) {
+ this.type = type;
+ }
+
+ @Override
+ public String toString() {
+ return getAtomTypeString(type);
+ }
+
+ /**
+ * An MP4 atom that is a leaf.
+ */
+ /* package */ static final class LeafAtom extends Atom {
+
+ /**
+ * The atom data.
+ */
+ public final ParsableByteArray data;
+
+ /**
+ * @param type The type of the atom.
+ * @param data The atom data.
+ */
+ public LeafAtom(int type, ParsableByteArray data) {
+ super(type);
+ this.data = data;
+ }
+
+ }
+
+ /**
+ * An MP4 atom that has child atoms.
+ */
+ /* package */ static final class ContainerAtom extends Atom {
+
+ public final long endPosition;
+ public final List<LeafAtom> leafChildren;
+ public final List<ContainerAtom> containerChildren;
+
+ /**
+ * @param type The type of the atom.
+ * @param endPosition The position of the first byte after the end of the atom.
+ */
+ public ContainerAtom(int type, long endPosition) {
+ super(type);
+ this.endPosition = endPosition;
+ leafChildren = new ArrayList<>();
+ containerChildren = new ArrayList<>();
+ }
+
+ /**
+ * Adds a child leaf to this container.
+ *
+ * @param atom The child to add.
+ */
+ public void add(LeafAtom atom) {
+ leafChildren.add(atom);
+ }
+
+ /**
+ * Adds a child container to this container.
+ *
+ * @param atom The child to add.
+ */
+ public void add(ContainerAtom atom) {
+ containerChildren.add(atom);
+ }
+
+ /**
+ * Returns the child leaf of the given type.
+ *
+ * <p>If no child exists with the given type then null is returned. If multiple children exist
+ * with the given type then the first one to have been added is returned.
+ *
+ * @param type The leaf type.
+ * @return The child leaf of the given type, or null if no such child exists.
+ */
+ @Nullable
+ public LeafAtom getLeafAtomOfType(int type) {
+ int childrenSize = leafChildren.size();
+ for (int i = 0; i < childrenSize; i++) {
+ LeafAtom atom = leafChildren.get(i);
+ if (atom.type == type) {
+ return atom;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the child container of the given type.
+ *
+ * <p>If no child exists with the given type then null is returned. If multiple children exist
+ * with the given type then the first one to have been added is returned.
+ *
+ * @param type The container type.
+ * @return The child container of the given type, or null if no such child exists.
+ */
+ @Nullable
+ public ContainerAtom getContainerAtomOfType(int type) {
+ int childrenSize = containerChildren.size();
+ for (int i = 0; i < childrenSize; i++) {
+ ContainerAtom atom = containerChildren.get(i);
+ if (atom.type == type) {
+ return atom;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the total number of leaf/container children of this atom with the given type.
+ *
+ * @param type The type of child atoms to count.
+ * @return The total number of leaf/container children of this atom with the given type.
+ */
+ public int getChildAtomOfTypeCount(int type) {
+ int count = 0;
+ int size = leafChildren.size();
+ for (int i = 0; i < size; i++) {
+ LeafAtom atom = leafChildren.get(i);
+ if (atom.type == type) {
+ count++;
+ }
+ }
+ size = containerChildren.size();
+ for (int i = 0; i < size; i++) {
+ ContainerAtom atom = containerChildren.get(i);
+ if (atom.type == type) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public String toString() {
+ return getAtomTypeString(type)
+ + " leaves: " + Arrays.toString(leafChildren.toArray())
+ + " containers: " + Arrays.toString(containerChildren.toArray());
+ }
+
+ }
+
+ /**
+ * Parses the version number out of the additional integer component of a full atom.
+ */
+ public static int parseFullAtomVersion(int fullAtomInt) {
+ return 0x000000FF & (fullAtomInt >> 24);
+ }
+
+ /**
+ * Parses the atom flags out of the additional integer component of a full atom.
+ */
+ public static int parseFullAtomFlags(int fullAtomInt) {
+ return 0x00FFFFFF & fullAtomInt;
+ }
+
+ /**
+ * Converts a numeric atom type to the corresponding four character string.
+ *
+ * @param type The numeric atom type.
+ * @return The corresponding four character string.
+ */
+ public static String getAtomTypeString(int type) {
+ return "" + (char) ((type >> 24) & 0xFF)
+ + (char) ((type >> 16) & 0xFF)
+ + (char) ((type >> 8) & 0xFF)
+ + (char) (type & 0xFF);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
new file mode 100644
index 0000000000..93ee2d6810
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
@@ -0,0 +1,1607 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType;
+
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.video.DolbyVisionConfig;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.video.HevcConfig;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */
+@SuppressWarnings({"ConstantField"})
+/* package */ final class AtomParsers {
+
+ private static final String TAG = "AtomParsers";
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ private static final int TYPE_vide = 0x76696465;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ private static final int TYPE_soun = 0x736f756e;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ private static final int TYPE_text = 0x74657874;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ private static final int TYPE_sbtl = 0x7362746c;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ private static final int TYPE_subt = 0x73756274;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ private static final int TYPE_clcp = 0x636c6370;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ private static final int TYPE_meta = 0x6d657461;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ private static final int TYPE_mdta = 0x6d647461;
+
+ /**
+ * The threshold number of samples to trim from the start/end of an audio track when applying an
+ * edit below which gapless info can be used (rather than removing samples from the sample table).
+ */
+ private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 4;
+
+ /** The magic signature for an Opus Identification header, as defined in RFC-7845. */
+ private static final byte[] opusMagic = Util.getUtf8Bytes("OpusHead");
+
+ /**
+ * Parses a trak atom (defined in 14496-12).
+ *
+ * @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
+ * {@link C#TIME_UNSET} if the duration should be parsed from the tkhd atom.
+ * @param drmInitData {@link DrmInitData} to be included in the format.
+ * @param ignoreEditLists Whether to ignore any edit lists in the trak box.
+ * @param isQuickTime True for QuickTime media. False otherwise.
+ * @return A {@link Track} instance, or {@code null} if the track's type isn't supported.
+ */
+ public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration,
+ DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime)
+ throws ParserException {
+ Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
+ int trackType = getTrackTypeForHdlr(parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data));
+ if (trackType == C.TRACK_TYPE_UNKNOWN) {
+ return null;
+ }
+
+ TkhdData tkhdData = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
+ if (duration == C.TIME_UNSET) {
+ duration = tkhdData.duration;
+ }
+ long movieTimescale = parseMvhd(mvhd.data);
+ long durationUs;
+ if (duration == C.TIME_UNSET) {
+ durationUs = C.TIME_UNSET;
+ } else {
+ durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, movieTimescale);
+ }
+ Atom.ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf)
+ .getContainerAtomOfType(Atom.TYPE_stbl);
+
+ Pair<Long, String> mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);
+ StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id,
+ tkhdData.rotationDegrees, mdhdData.second, drmInitData, isQuickTime);
+ long[] editListDurations = null;
+ long[] editListMediaTimes = null;
+ if (!ignoreEditLists) {
+ Pair<long[], long[]> edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts));
+ editListDurations = edtsData.first;
+ editListMediaTimes = edtsData.second;
+ }
+ return stsdData.format == null ? null
+ : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs,
+ stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes,
+ stsdData.nalUnitLengthFieldLength, editListDurations, editListMediaTimes);
+ }
+
+ /**
+ * 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 Thrown if the stbl atom can't be parsed.
+ */
+ 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(
+ track,
+ /* offsets= */ new long[0],
+ /* sizes= */ new int[0],
+ /* maximumSize= */ 0,
+ /* timestampsUs= */ new long[0],
+ /* flags= */ new int[0],
+ /* durationUs= */ C.TIME_UNSET);
+ }
+
+ // 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;
+ }
+ }
+
+ // Fixed sample size raw audio may need to be rechunked.
+ boolean isFixedSampleSizeRawAudio =
+ 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;
+ long duration;
+
+ if (!isFixedSampleSizeRawAudio) {
+ 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.
+ boolean chunkDataComplete = true;
+ while (remainingSamplesInChunk == 0 && (chunkDataComplete = chunkIterator.moveNext())) {
+ offset = chunkIterator.offset;
+ remainingSamplesInChunk = chunkIterator.numSamples;
+ }
+ if (!chunkDataComplete) {
+ Log.w(TAG, "Unexpected end of chunk data");
+ sampleCount = i;
+ offsets = Arrays.copyOf(offsets, sampleCount);
+ sizes = Arrays.copyOf(sizes, sampleCount);
+ timestamps = Arrays.copyOf(timestamps, sampleCount);
+ flags = Arrays.copyOf(flags, sampleCount);
+ break;
+ }
+
+ // 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();
+ // The BMFF spec (ISO 14496-12) states that sample deltas should be unsigned integers
+ // in stts boxes, however some streams violate the spec and use signed integers instead.
+ // See https://github.com/google/ExoPlayer/issues/3384. It's safe to always decode sample
+ // deltas 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
+ // deltas are always small).
+ timestampDeltaInTimeUnits = stts.readInt();
+ remainingTimestampDeltaChanges--;
+ }
+
+ offset += sizes[i];
+ remainingSamplesInChunk--;
+ }
+ duration = timestampTimeUnits + timestampOffset;
+
+ // If the stbl's child boxes are not consistent the container is malformed, but the stream may
+ // still be playable.
+ boolean isCttsValid = true;
+ while (remainingTimestampOffsetChanges > 0) {
+ if (ctts.readUnsignedIntToInt() != 0) {
+ isCttsValid = false;
+ break;
+ }
+ ctts.readInt(); // Ignore offset.
+ remainingTimestampOffsetChanges--;
+ }
+ if (remainingSynchronizationSamples != 0
+ || remainingSamplesAtTimestampDelta != 0
+ || remainingSamplesInChunk != 0
+ || remainingTimestampDeltaChanges != 0
+ || remainingSamplesAtTimestampOffset != 0
+ || !isCttsValid) {
+ Log.w(
+ TAG,
+ "Inconsistent stbl box for track "
+ + track.id
+ + ": remainingSynchronizationSamples "
+ + remainingSynchronizationSamples
+ + ", remainingSamplesAtTimestampDelta "
+ + remainingSamplesAtTimestampDelta
+ + ", remainingSamplesInChunk "
+ + remainingSamplesInChunk
+ + ", remainingTimestampDeltaChanges "
+ + remainingTimestampDeltaChanges
+ + ", remainingSamplesAtTimestampOffset "
+ + remainingSamplesAtTimestampOffset
+ + (!isCttsValid ? ", ctts invalid" : ""));
+ }
+ } 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 =
+ Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount);
+ FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk(
+ fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
+ offsets = rechunkedResults.offsets;
+ sizes = rechunkedResults.sizes;
+ maximumSize = rechunkedResults.maximumSize;
+ timestamps = rechunkedResults.timestamps;
+ flags = rechunkedResults.flags;
+ duration = rechunkedResults.duration;
+ }
+ long durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, track.timescale);
+
+ if (track.editListDurations == null) {
+ Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
+ return new TrackSampleTable(
+ track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
+ }
+
+ // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a
+ // sync sample after reordering are not supported. Partial audio sample truncation is only
+ // supported in edit lists with one edit that removes less than MAX_GAPLESS_TRIM_SIZE_SAMPLES
+ // samples from the start/end of the track. This implementation handles simple
+ // discarding/delaying of samples. The extractor may place further restrictions on what edited
+ // streams are playable.
+
+ if (track.editListDurations.length == 1
+ && track.type == C.TRACK_TYPE_AUDIO
+ && timestamps.length >= 2) {
+ long editStartTime = track.editListMediaTimes[0];
+ long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0],
+ track.timescale, track.movieTimescale);
+ if (canApplyEditWithGaplessInfo(timestamps, duration, editStartTime, editEndTime)) {
+ long paddingTimeUnits = duration - 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);
+ long editedDurationUs =
+ Util.scaleLargeTimestamp(
+ track.editListDurations[0], C.MICROS_PER_SECOND, track.movieTimescale);
+ return new TrackSampleTable(
+ track, offsets, sizes, maximumSize, timestamps, flags, editedDurationUs);
+ }
+ }
+ }
+
+ if (track.editListDurations.length == 1 && track.editListDurations[0] == 0) {
+ // The current version of the spec leaves handling of an edit with zero segment_duration in
+ // unfragmented files open to interpretation. We handle this as a special case and include all
+ // samples in the edit.
+ long editStartTime = track.editListMediaTimes[0];
+ for (int i = 0; i < timestamps.length; i++) {
+ timestamps[i] =
+ Util.scaleLargeTimestamp(
+ timestamps[i] - editStartTime, C.MICROS_PER_SECOND, track.timescale);
+ }
+ durationUs =
+ Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale);
+ return new TrackSampleTable(
+ track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
+ }
+
+ // 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;
+ int[] startIndices = new int[track.editListDurations.length];
+ int[] endIndices = new int[track.editListDurations.length];
+ for (int i = 0; i < track.editListDurations.length; i++) {
+ long editMediaTime = track.editListMediaTimes[i];
+ if (editMediaTime != -1) {
+ long editDuration =
+ Util.scaleLargeTimestamp(
+ track.editListDurations[i], track.timescale, track.movieTimescale);
+ startIndices[i] =
+ Util.binarySearchFloor(
+ timestamps, editMediaTime, /* inclusive= */ true, /* stayInBounds= */ true);
+ endIndices[i] =
+ Util.binarySearchCeil(
+ timestamps,
+ editMediaTime + editDuration,
+ /* inclusive= */ omitClippedSample,
+ /* stayInBounds= */ false);
+ while (startIndices[i] < endIndices[i]
+ && (flags[startIndices[i]] & C.BUFFER_FLAG_KEY_FRAME) == 0) {
+ // Applying the edit correctly would require prerolling from the previous sync sample. In
+ // the current implementation we advance to the next sync sample instead. Only other
+ // tracks (i.e. audio) will be rendered until the time of the first sync sample.
+ // See https://github.com/google/ExoPlayer/issues/1659.
+ startIndices[i]++;
+ }
+ editedSampleCount += endIndices[i] - startIndices[i];
+ copyMetadata |= nextSampleIndex != startIndices[i];
+ nextSampleIndex = endIndices[i];
+ }
+ }
+ 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 editMediaTime = track.editListMediaTimes[i];
+ int startIndex = startIndices[i];
+ int endIndex = endIndices[i];
+ 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(
+ Math.max(0, timestamps[j] - editMediaTime), C.MICROS_PER_SECOND, track.timescale);
+ editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs;
+ if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) {
+ editedMaximumSize = sizes[j];
+ }
+ sampleIndex++;
+ }
+ pts += track.editListDurations[i];
+ }
+ long editedDurationUs =
+ Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
+ return new TrackSampleTable(
+ track,
+ editedOffsets,
+ editedSizes,
+ editedMaximumSize,
+ editedTimestamps,
+ editedFlags,
+ editedDurationUs);
+ }
+
+ /**
+ * Parses a udta atom.
+ *
+ * @param udtaAtom The udta (user data) atom to decode.
+ * @param isQuickTime True for QuickTime media. False otherwise.
+ * @return Parsed metadata, or null.
+ */
+ @Nullable
+ public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {
+ if (isQuickTime) {
+ // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
+ // decode one.
+ return null;
+ }
+ ParsableByteArray udtaData = udtaAtom.data;
+ udtaData.setPosition(Atom.HEADER_SIZE);
+ while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) {
+ int atomPosition = udtaData.getPosition();
+ int atomSize = udtaData.readInt();
+ int atomType = udtaData.readInt();
+ if (atomType == Atom.TYPE_meta) {
+ udtaData.setPosition(atomPosition);
+ return parseUdtaMeta(udtaData, atomPosition + atomSize);
+ }
+ udtaData.setPosition(atomPosition + atomSize);
+ }
+ return null;
+ }
+
+ /**
+ * Parses a metadata meta atom if it contains metadata with handler 'mdta'.
+ *
+ * @param meta The metadata atom to decode.
+ * @return Parsed metadata, or null.
+ */
+ @Nullable
+ public static Metadata parseMdtaFromMeta(Atom.ContainerAtom meta) {
+ Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr);
+ Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys);
+ Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst);
+ if (hdlrAtom == null
+ || keysAtom == null
+ || ilstAtom == null
+ || AtomParsers.parseHdlr(hdlrAtom.data) != TYPE_mdta) {
+ // There isn't enough information to parse the metadata, or the handler type is unexpected.
+ return null;
+ }
+
+ // Parse metadata keys.
+ ParsableByteArray keys = keysAtom.data;
+ keys.setPosition(Atom.FULL_HEADER_SIZE);
+ int entryCount = keys.readInt();
+ String[] keyNames = new String[entryCount];
+ for (int i = 0; i < entryCount; i++) {
+ int entrySize = keys.readInt();
+ keys.skipBytes(4); // keyNamespace
+ int keySize = entrySize - 8;
+ keyNames[i] = keys.readString(keySize);
+ }
+
+ // Parse metadata items.
+ ParsableByteArray ilst = ilstAtom.data;
+ ilst.setPosition(Atom.HEADER_SIZE);
+ ArrayList<Metadata.Entry> entries = new ArrayList<>();
+ while (ilst.bytesLeft() > Atom.HEADER_SIZE) {
+ int atomPosition = ilst.getPosition();
+ int atomSize = ilst.readInt();
+ int keyIndex = ilst.readInt() - 1;
+ if (keyIndex >= 0 && keyIndex < keyNames.length) {
+ String key = keyNames[keyIndex];
+ Metadata.Entry entry =
+ MetadataUtil.parseMdtaMetadataEntryFromIlst(ilst, atomPosition + atomSize, key);
+ if (entry != null) {
+ entries.add(entry);
+ }
+ } else {
+ Log.w(TAG, "Skipped metadata with unknown key index: " + keyIndex);
+ }
+ ilst.setPosition(atomPosition + atomSize);
+ }
+ return entries.isEmpty() ? null : new Metadata(entries);
+ }
+
+ @Nullable
+ private static Metadata parseUdtaMeta(ParsableByteArray meta, int limit) {
+ meta.skipBytes(Atom.FULL_HEADER_SIZE);
+ while (meta.getPosition() < limit) {
+ int atomPosition = meta.getPosition();
+ int atomSize = meta.readInt();
+ int atomType = meta.readInt();
+ if (atomType == Atom.TYPE_ilst) {
+ meta.setPosition(atomPosition);
+ return parseIlst(meta, atomPosition + atomSize);
+ }
+ meta.setPosition(atomPosition + atomSize);
+ }
+ return null;
+ }
+
+ @Nullable
+ private static Metadata parseIlst(ParsableByteArray ilst, int limit) {
+ ilst.skipBytes(Atom.HEADER_SIZE);
+ ArrayList<Metadata.Entry> entries = new ArrayList<>();
+ while (ilst.getPosition() < limit) {
+ Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst);
+ if (entry != null) {
+ entries.add(entry);
+ }
+ }
+ return entries.isEmpty() ? null : new Metadata(entries);
+ }
+
+ /**
+ * Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie.
+ *
+ * @param mvhd Contents of the mvhd atom to be parsed.
+ * @return Timescale for the movie.
+ */
+ private static long parseMvhd(ParsableByteArray mvhd) {
+ mvhd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = mvhd.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ mvhd.skipBytes(version == 0 ? 8 : 16);
+ return mvhd.readUnsignedInt();
+ }
+
+ /**
+ * Parses a tkhd atom (defined in 14496-12).
+ *
+ * @return An object containing the parsed data.
+ */
+ private static TkhdData parseTkhd(ParsableByteArray tkhd) {
+ tkhd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = tkhd.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+
+ tkhd.skipBytes(version == 0 ? 8 : 16);
+ int trackId = tkhd.readInt();
+
+ tkhd.skipBytes(4);
+ boolean durationUnknown = true;
+ int durationPosition = tkhd.getPosition();
+ int durationByteCount = version == 0 ? 4 : 8;
+ for (int i = 0; i < durationByteCount; i++) {
+ if (tkhd.data[durationPosition + i] != -1) {
+ durationUnknown = false;
+ break;
+ }
+ }
+ long duration;
+ if (durationUnknown) {
+ tkhd.skipBytes(durationByteCount);
+ duration = C.TIME_UNSET;
+ } else {
+ duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong();
+ if (duration == 0) {
+ // 0 duration normally indicates that the file is fully fragmented (i.e. all of the media
+ // samples are in fragments). Treat as unknown.
+ duration = C.TIME_UNSET;
+ }
+ }
+
+ tkhd.skipBytes(16);
+ int a00 = tkhd.readInt();
+ int a01 = tkhd.readInt();
+ tkhd.skipBytes(4);
+ int a10 = tkhd.readInt();
+ int a11 = tkhd.readInt();
+
+ int rotationDegrees;
+ int fixedOne = 65536;
+ if (a00 == 0 && a01 == fixedOne && a10 == -fixedOne && a11 == 0) {
+ rotationDegrees = 90;
+ } else if (a00 == 0 && a01 == -fixedOne && a10 == fixedOne && a11 == 0) {
+ rotationDegrees = 270;
+ } else if (a00 == -fixedOne && a01 == 0 && a10 == 0 && a11 == -fixedOne) {
+ rotationDegrees = 180;
+ } else {
+ // Only 0, 90, 180 and 270 are supported. Treat anything else as 0.
+ rotationDegrees = 0;
+ }
+
+ return new TkhdData(trackId, duration, rotationDegrees);
+ }
+
+ /**
+ * Parses an hdlr atom.
+ *
+ * @param hdlr The hdlr atom to decode.
+ * @return The handler value.
+ */
+ private static int parseHdlr(ParsableByteArray hdlr) {
+ hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
+ return hdlr.readInt();
+ }
+
+ /** Returns the track type for a given handler value. */
+ private static int getTrackTypeForHdlr(int hdlr) {
+ if (hdlr == TYPE_soun) {
+ return C.TRACK_TYPE_AUDIO;
+ } else if (hdlr == TYPE_vide) {
+ return C.TRACK_TYPE_VIDEO;
+ } else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) {
+ return C.TRACK_TYPE_TEXT;
+ } else if (hdlr == TYPE_meta) {
+ return C.TRACK_TYPE_METADATA;
+ } else {
+ return C.TRACK_TYPE_UNKNOWN;
+ }
+ }
+
+ /**
+ * Parses an mdhd atom (defined in 14496-12).
+ *
+ * @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.
+ */
+ private static Pair<Long, String> parseMdhd(ParsableByteArray mdhd) {
+ mdhd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = mdhd.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ mdhd.skipBytes(version == 0 ? 8 : 16);
+ long timescale = mdhd.readUnsignedInt();
+ mdhd.skipBytes(version == 0 ? 4 : 8);
+ int languageCode = mdhd.readUnsignedShort();
+ String language =
+ ""
+ + (char) (((languageCode >> 10) & 0x1F) + 0x60)
+ + (char) (((languageCode >> 5) & 0x1F) + 0x60)
+ + (char) ((languageCode & 0x1F) + 0x60);
+ return Pair.create(timescale, language);
+ }
+
+ /**
+ * Parses a stsd atom (defined in 14496-12).
+ *
+ * @param stsd The stsd atom to decode.
+ * @param trackId The track's identifier in its container.
+ * @param rotationDegrees The rotation of the track in degrees.
+ * @param language The language of the track.
+ * @param drmInitData {@link DrmInitData} to be included in the format.
+ * @param isQuickTime True for QuickTime media. False otherwise.
+ * @return An object containing the parsed data.
+ */
+ private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees,
+ String language, DrmInitData drmInitData, boolean isQuickTime) throws ParserException {
+ stsd.setPosition(Atom.FULL_HEADER_SIZE);
+ int numberOfEntries = stsd.readInt();
+ StsdData out = new StsdData(numberOfEntries);
+ for (int i = 0; i < numberOfEntries; i++) {
+ int childStartPosition = stsd.getPosition();
+ int childAtomSize = stsd.readInt();
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childAtomType = stsd.readInt();
+ if (childAtomType == Atom.TYPE_avc1
+ || childAtomType == Atom.TYPE_avc3
+ || childAtomType == Atom.TYPE_encv
+ || childAtomType == Atom.TYPE_mp4v
+ || childAtomType == Atom.TYPE_hvc1
+ || childAtomType == Atom.TYPE_hev1
+ || childAtomType == Atom.TYPE_s263
+ || childAtomType == Atom.TYPE_vp08
+ || childAtomType == Atom.TYPE_vp09
+ || childAtomType == Atom.TYPE_av01
+ || childAtomType == Atom.TYPE_dvav
+ || childAtomType == Atom.TYPE_dva1
+ || childAtomType == Atom.TYPE_dvhe
+ || childAtomType == Atom.TYPE_dvh1) {
+ parseVideoSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
+ rotationDegrees, drmInitData, out, i);
+ } else if (childAtomType == Atom.TYPE_mp4a
+ || childAtomType == Atom.TYPE_enca
+ || childAtomType == Atom.TYPE_ac_3
+ || childAtomType == Atom.TYPE_ec_3
+ || childAtomType == Atom.TYPE_ac_4
+ || childAtomType == Atom.TYPE_dtsc
+ || childAtomType == Atom.TYPE_dtse
+ || childAtomType == Atom.TYPE_dtsh
+ || childAtomType == Atom.TYPE_dtsl
+ || childAtomType == Atom.TYPE_samr
+ || childAtomType == Atom.TYPE_sawb
+ || childAtomType == Atom.TYPE_lpcm
+ || childAtomType == Atom.TYPE_sowt
+ || childAtomType == Atom.TYPE_twos
+ || childAtomType == Atom.TYPE__mp3
+ || childAtomType == Atom.TYPE_alac
+ || childAtomType == Atom.TYPE_alaw
+ || childAtomType == Atom.TYPE_ulaw
+ || childAtomType == Atom.TYPE_Opus
+ || childAtomType == Atom.TYPE_fLaC) {
+ parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
+ language, isQuickTime, drmInitData, out, i);
+ } else if (childAtomType == Atom.TYPE_TTML || childAtomType == Atom.TYPE_tx3g
+ || childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp
+ || childAtomType == Atom.TYPE_c608) {
+ parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
+ language, out);
+ } else if (childAtomType == Atom.TYPE_camm) {
+ out.format = Format.createSampleFormat(Integer.toString(trackId),
+ MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, null);
+ }
+ stsd.setPosition(childStartPosition + childAtomSize);
+ }
+ return out;
+ }
+
+ private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position,
+ int atomSize, int trackId, String language, StsdData out) throws ParserException {
+ parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
+
+ // Default values.
+ List<byte[]> initializationData = null;
+ long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE;
+
+ String mimeType;
+ if (atomType == Atom.TYPE_TTML) {
+ mimeType = MimeTypes.APPLICATION_TTML;
+ } else if (atomType == Atom.TYPE_tx3g) {
+ mimeType = MimeTypes.APPLICATION_TX3G;
+ int sampleDescriptionLength = atomSize - Atom.HEADER_SIZE - 8;
+ byte[] sampleDescriptionData = new byte[sampleDescriptionLength];
+ parent.readBytes(sampleDescriptionData, 0, sampleDescriptionLength);
+ initializationData = Collections.singletonList(sampleDescriptionData);
+ } else if (atomType == Atom.TYPE_wvtt) {
+ mimeType = MimeTypes.APPLICATION_MP4VTT;
+ } else if (atomType == Atom.TYPE_stpp) {
+ mimeType = MimeTypes.APPLICATION_TTML;
+ subsampleOffsetUs = 0; // Subsample timing is absolute.
+ } else if (atomType == Atom.TYPE_c608) {
+ // Defined by the QuickTime File Format specification.
+ mimeType = MimeTypes.APPLICATION_MP4CEA608;
+ out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT;
+ } else {
+ // Never happens.
+ throw new IllegalStateException();
+ }
+
+ out.format =
+ Format.createTextSampleFormat(
+ Integer.toString(trackId),
+ mimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ /* selectionFlags= */ 0,
+ language,
+ /* accessibilityChannel= */ Format.NO_VALUE,
+ /* drmInitData= */ null,
+ subsampleOffsetUs,
+ initializationData);
+ }
+
+ private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position,
+ int size, int trackId, int rotationDegrees, DrmInitData drmInitData, StsdData out,
+ int entryIndex) throws ParserException {
+ parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
+
+ parent.skipBytes(16);
+ int width = parent.readUnsignedShort();
+ int height = parent.readUnsignedShort();
+ boolean pixelWidthHeightRatioFromPasp = false;
+ float pixelWidthHeightRatio = 1;
+ parent.skipBytes(50);
+
+ int childPosition = parent.getPosition();
+ if (atomType == Atom.TYPE_encv) {
+ Pair<Integer, TrackEncryptionBox> sampleEntryEncryptionData = parseSampleEntryEncryptionData(
+ parent, position, size);
+ if (sampleEntryEncryptionData != null) {
+ atomType = sampleEntryEncryptionData.first;
+ drmInitData = drmInitData == null ? null
+ : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType);
+ out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second;
+ }
+ parent.setPosition(childPosition);
+ }
+ // TODO: Uncomment when [Internal: b/63092960] is fixed.
+ // else {
+ // drmInitData = null;
+ // }
+
+ List<byte[]> initializationData = null;
+ String mimeType = null;
+ String codecs = null;
+ byte[] projectionData = null;
+ @C.StereoMode
+ int stereoMode = Format.NO_VALUE;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childStartPosition = parent.getPosition();
+ int childAtomSize = parent.readInt();
+ if (childAtomSize == 0 && parent.getPosition() - position == size) {
+ // Handle optional terminating four zero bytes in MOV files.
+ break;
+ }
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_avcC) {
+ Assertions.checkState(mimeType == null);
+ mimeType = MimeTypes.VIDEO_H264;
+ parent.setPosition(childStartPosition + Atom.HEADER_SIZE);
+ AvcConfig avcConfig = AvcConfig.parse(parent);
+ initializationData = avcConfig.initializationData;
+ out.nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;
+ if (!pixelWidthHeightRatioFromPasp) {
+ pixelWidthHeightRatio = avcConfig.pixelWidthAspectRatio;
+ }
+ } else if (childAtomType == Atom.TYPE_hvcC) {
+ Assertions.checkState(mimeType == null);
+ mimeType = MimeTypes.VIDEO_H265;
+ parent.setPosition(childStartPosition + Atom.HEADER_SIZE);
+ HevcConfig hevcConfig = HevcConfig.parse(parent);
+ initializationData = hevcConfig.initializationData;
+ out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength;
+ } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) {
+ DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent);
+ if (dolbyVisionConfig != null) {
+ codecs = dolbyVisionConfig.codecs;
+ mimeType = MimeTypes.VIDEO_DOLBY_VISION;
+ }
+ } else if (childAtomType == Atom.TYPE_vpcC) {
+ Assertions.checkState(mimeType == null);
+ mimeType = (atomType == Atom.TYPE_vp08) ? MimeTypes.VIDEO_VP8 : MimeTypes.VIDEO_VP9;
+ } else if (childAtomType == Atom.TYPE_av1C) {
+ Assertions.checkState(mimeType == null);
+ mimeType = MimeTypes.VIDEO_AV1;
+ } else if (childAtomType == Atom.TYPE_d263) {
+ Assertions.checkState(mimeType == null);
+ mimeType = MimeTypes.VIDEO_H263;
+ } else if (childAtomType == Atom.TYPE_esds) {
+ Assertions.checkState(mimeType == null);
+ Pair<String, byte[]> mimeTypeAndInitializationData =
+ parseEsdsFromParent(parent, childStartPosition);
+ mimeType = mimeTypeAndInitializationData.first;
+ initializationData = Collections.singletonList(mimeTypeAndInitializationData.second);
+ } else if (childAtomType == Atom.TYPE_pasp) {
+ pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition);
+ pixelWidthHeightRatioFromPasp = true;
+ } else if (childAtomType == Atom.TYPE_sv3d) {
+ projectionData = parseProjFromParent(parent, childStartPosition, childAtomSize);
+ } else if (childAtomType == Atom.TYPE_st3d) {
+ int version = parent.readUnsignedByte();
+ parent.skipBytes(3); // Flags.
+ if (version == 0) {
+ int layout = parent.readUnsignedByte();
+ switch (layout) {
+ case 0:
+ stereoMode = C.STEREO_MODE_MONO;
+ break;
+ case 1:
+ stereoMode = C.STEREO_MODE_TOP_BOTTOM;
+ break;
+ case 2:
+ stereoMode = C.STEREO_MODE_LEFT_RIGHT;
+ break;
+ case 3:
+ stereoMode = C.STEREO_MODE_STEREO_MESH;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ childPosition += childAtomSize;
+ }
+
+ // If the media type was not recognized, ignore the track.
+ if (mimeType == null) {
+ return;
+ }
+
+ out.format =
+ Format.createVideoSampleFormat(
+ Integer.toString(trackId),
+ mimeType,
+ codecs,
+ /* bitrate= */ Format.NO_VALUE,
+ /* maxInputSize= */ Format.NO_VALUE,
+ width,
+ height,
+ /* frameRate= */ Format.NO_VALUE,
+ initializationData,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ /* colorInfo= */ null,
+ drmInitData);
+ }
+
+ /**
+ * Parses the edts atom (defined in 14496-12 subsection 8.6.5).
+ *
+ * @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.
+ */
+ private static Pair<long[], long[]> parseEdts(Atom.ContainerAtom edtsAtom) {
+ Atom.LeafAtom elst;
+ if (edtsAtom == null || (elst = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst)) == null) {
+ return Pair.create(null, null);
+ }
+ ParsableByteArray elstData = elst.data;
+ elstData.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = elstData.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ int entryCount = elstData.readUnsignedIntToInt();
+ long[] editListDurations = new long[entryCount];
+ long[] editListMediaTimes = new long[entryCount];
+ for (int i = 0; i < entryCount; i++) {
+ editListDurations[i] =
+ version == 1 ? elstData.readUnsignedLongToLong() : elstData.readUnsignedInt();
+ editListMediaTimes[i] = version == 1 ? elstData.readLong() : elstData.readInt();
+ int mediaRateInteger = elstData.readShort();
+ if (mediaRateInteger != 1) {
+ // The extractor does not handle dwell edits (mediaRateInteger == 0).
+ throw new IllegalArgumentException("Unsupported media rate.");
+ }
+ elstData.skipBytes(2);
+ }
+ return Pair.create(editListDurations, editListMediaTimes);
+ }
+
+ private static float parsePaspFromParent(ParsableByteArray parent, int position) {
+ parent.setPosition(position + Atom.HEADER_SIZE);
+ int hSpacing = parent.readUnsignedIntToInt();
+ int vSpacing = parent.readUnsignedIntToInt();
+ return (float) hSpacing / vSpacing;
+ }
+
+ private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position,
+ int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData,
+ StsdData out, int entryIndex) throws ParserException {
+ parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
+
+ int quickTimeSoundDescriptionVersion = 0;
+ if (isQuickTime) {
+ quickTimeSoundDescriptionVersion = parent.readUnsignedShort();
+ parent.skipBytes(6);
+ } else {
+ parent.skipBytes(8);
+ }
+
+ int channelCount;
+ int sampleRate;
+ @C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
+
+ if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) {
+ channelCount = parent.readUnsignedShort();
+ parent.skipBytes(6); // sampleSize, compressionId, packetSize.
+ sampleRate = parent.readUnsignedFixedPoint1616();
+
+ if (quickTimeSoundDescriptionVersion == 1) {
+ parent.skipBytes(16);
+ }
+ } else if (quickTimeSoundDescriptionVersion == 2) {
+ parent.skipBytes(16); // always[3,16,Minus2,0,65536], sizeOfStructOnly
+
+ sampleRate = (int) Math.round(parent.readDouble());
+ channelCount = parent.readUnsignedIntToInt();
+
+ // Skip always7F000000, sampleSize, formatSpecificFlags, constBytesPerAudioPacket,
+ // constLPCMFramesPerAudioPacket.
+ parent.skipBytes(20);
+ } else {
+ // Unsupported version.
+ return;
+ }
+
+ int childPosition = parent.getPosition();
+ if (atomType == Atom.TYPE_enca) {
+ Pair<Integer, TrackEncryptionBox> sampleEntryEncryptionData = parseSampleEntryEncryptionData(
+ parent, position, size);
+ if (sampleEntryEncryptionData != null) {
+ atomType = sampleEntryEncryptionData.first;
+ drmInitData = drmInitData == null ? null
+ : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType);
+ out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second;
+ }
+ parent.setPosition(childPosition);
+ }
+ // TODO: Uncomment when [Internal: b/63092960] is fixed.
+ // else {
+ // drmInitData = null;
+ // }
+
+ // If the atom type determines a MIME type, set it immediately.
+ String mimeType = null;
+ if (atomType == Atom.TYPE_ac_3) {
+ mimeType = MimeTypes.AUDIO_AC3;
+ } else if (atomType == Atom.TYPE_ec_3) {
+ mimeType = MimeTypes.AUDIO_E_AC3;
+ } else if (atomType == Atom.TYPE_ac_4) {
+ mimeType = MimeTypes.AUDIO_AC4;
+ } else if (atomType == Atom.TYPE_dtsc) {
+ mimeType = MimeTypes.AUDIO_DTS;
+ } else if (atomType == Atom.TYPE_dtsh || atomType == Atom.TYPE_dtsl) {
+ mimeType = MimeTypes.AUDIO_DTS_HD;
+ } else if (atomType == Atom.TYPE_dtse) {
+ mimeType = MimeTypes.AUDIO_DTS_EXPRESS;
+ } else if (atomType == Atom.TYPE_samr) {
+ mimeType = MimeTypes.AUDIO_AMR_NB;
+ } else if (atomType == Atom.TYPE_sawb) {
+ mimeType = MimeTypes.AUDIO_AMR_WB;
+ } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) {
+ mimeType = MimeTypes.AUDIO_RAW;
+ pcmEncoding = C.ENCODING_PCM_16BIT;
+ } else if (atomType == Atom.TYPE_twos) {
+ mimeType = MimeTypes.AUDIO_RAW;
+ pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN;
+ } else if (atomType == Atom.TYPE__mp3) {
+ mimeType = MimeTypes.AUDIO_MPEG;
+ } else if (atomType == Atom.TYPE_alac) {
+ mimeType = MimeTypes.AUDIO_ALAC;
+ } else if (atomType == Atom.TYPE_alaw) {
+ mimeType = MimeTypes.AUDIO_ALAW;
+ } else if (atomType == Atom.TYPE_ulaw) {
+ mimeType = MimeTypes.AUDIO_MLAW;
+ } else if (atomType == Atom.TYPE_Opus) {
+ mimeType = MimeTypes.AUDIO_OPUS;
+ } else if (atomType == Atom.TYPE_fLaC) {
+ mimeType = MimeTypes.AUDIO_FLAC;
+ }
+
+ byte[] initializationData = null;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_esds || (isQuickTime && childAtomType == Atom.TYPE_wave)) {
+ int esdsAtomPosition = childAtomType == Atom.TYPE_esds ? childPosition
+ : findEsdsPosition(parent, childPosition, childAtomSize);
+ if (esdsAtomPosition != C.POSITION_UNSET) {
+ Pair<String, byte[]> mimeTypeAndInitializationData =
+ parseEsdsFromParent(parent, esdsAtomPosition);
+ mimeType = mimeTypeAndInitializationData.first;
+ initializationData = mimeTypeAndInitializationData.second;
+ if (MimeTypes.AUDIO_AAC.equals(mimeType)) {
+ // Update sampleRate and channelCount from the AudioSpecificConfig initialization data,
+ // which is more reliable. See [Internal: b/10903778].
+ Pair<Integer, Integer> audioSpecificConfig =
+ CodecSpecificDataUtil.parseAacAudioSpecificConfig(initializationData);
+ sampleRate = audioSpecificConfig.first;
+ channelCount = audioSpecificConfig.second;
+ }
+ }
+ } else if (childAtomType == Atom.TYPE_dac3) {
+ parent.setPosition(Atom.HEADER_SIZE + childPosition);
+ out.format = Ac3Util.parseAc3AnnexFFormat(parent, Integer.toString(trackId), language,
+ drmInitData);
+ } else if (childAtomType == Atom.TYPE_dec3) {
+ parent.setPosition(Atom.HEADER_SIZE + childPosition);
+ out.format = Ac3Util.parseEAc3AnnexFFormat(parent, Integer.toString(trackId), language,
+ drmInitData);
+ } else if (childAtomType == Atom.TYPE_dac4) {
+ parent.setPosition(Atom.HEADER_SIZE + childPosition);
+ out.format =
+ Ac4Util.parseAc4AnnexEFormat(parent, Integer.toString(trackId), language, drmInitData);
+ } else if (childAtomType == Atom.TYPE_ddts) {
+ out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0,
+ language);
+ } else if (childAtomType == Atom.TYPE_dOps) {
+ // Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic
+ // Signature and the body of the dOps atom.
+ int childAtomBodySize = childAtomSize - Atom.HEADER_SIZE;
+ initializationData = new byte[opusMagic.length + childAtomBodySize];
+ System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length);
+ parent.setPosition(childPosition + Atom.HEADER_SIZE);
+ parent.readBytes(initializationData, opusMagic.length, childAtomBodySize);
+ } else if (childAtomType == Atom.TYPE_dfLa) {
+ int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE;
+ initializationData = new byte[4 + childAtomBodySize];
+ initializationData[0] = 0x66; // f
+ initializationData[1] = 0x4C; // L
+ initializationData[2] = 0x61; // a
+ initializationData[3] = 0x43; // C
+ parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE);
+ parent.readBytes(initializationData, /* offset= */ 4, childAtomBodySize);
+ } else if (childAtomType == Atom.TYPE_alac) {
+ int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE;
+ initializationData = new byte[childAtomBodySize];
+ parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE);
+ parent.readBytes(initializationData, /* offset= */ 0, childAtomBodySize);
+ // Update sampleRate and channelCount from the AudioSpecificConfig initialization data,
+ // which is more reliable. See https://github.com/google/ExoPlayer/pull/6629.
+ Pair<Integer, Integer> audioSpecificConfig =
+ CodecSpecificDataUtil.parseAlacAudioSpecificConfig(initializationData);
+ sampleRate = audioSpecificConfig.first;
+ channelCount = audioSpecificConfig.second;
+ }
+ childPosition += childAtomSize;
+ }
+
+ if (out.format == null && mimeType != null) {
+ out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding,
+ initializationData == null ? null : Collections.singletonList(initializationData),
+ drmInitData, 0, language);
+ }
+ }
+
+ /**
+ * Returns the position of the esds box within a parent, or {@link C#POSITION_UNSET} if no esds
+ * box is found
+ */
+ private static int findEsdsPosition(ParsableByteArray parent, int position, int size) {
+ int childAtomPosition = parent.getPosition();
+ while (childAtomPosition - position < size) {
+ parent.setPosition(childAtomPosition);
+ int childAtomSize = parent.readInt();
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childType = parent.readInt();
+ if (childType == Atom.TYPE_esds) {
+ return childAtomPosition;
+ }
+ childAtomPosition += childAtomSize;
+ }
+ return C.POSITION_UNSET;
+ }
+
+ /**
+ * Returns codec-specific initialization data contained in an esds box.
+ */
+ private static Pair<String, byte[]> parseEsdsFromParent(ParsableByteArray parent, int position) {
+ parent.setPosition(position + Atom.HEADER_SIZE + 4);
+ // Start of the ES_Descriptor (defined in 14496-1)
+ parent.skipBytes(1); // ES_Descriptor tag
+ parseExpandableClassSize(parent);
+ parent.skipBytes(2); // ES_ID
+
+ int flags = parent.readUnsignedByte();
+ if ((flags & 0x80 /* streamDependenceFlag */) != 0) {
+ parent.skipBytes(2);
+ }
+ if ((flags & 0x40 /* URL_Flag */) != 0) {
+ parent.skipBytes(parent.readUnsignedShort());
+ }
+ if ((flags & 0x20 /* OCRstreamFlag */) != 0) {
+ parent.skipBytes(2);
+ }
+
+ // Start of the DecoderConfigDescriptor (defined in 14496-1)
+ parent.skipBytes(1); // DecoderConfigDescriptor tag
+ parseExpandableClassSize(parent);
+
+ // Set the MIME type based on the object type indication (14496-1 table 5).
+ int objectTypeIndication = parent.readUnsignedByte();
+ String mimeType = getMimeTypeFromMp4ObjectType(objectTypeIndication);
+ if (MimeTypes.AUDIO_MPEG.equals(mimeType)
+ || MimeTypes.AUDIO_DTS.equals(mimeType)
+ || MimeTypes.AUDIO_DTS_HD.equals(mimeType)) {
+ return Pair.create(mimeType, null);
+ }
+
+ parent.skipBytes(12);
+
+ // Start of the DecoderSpecificInfo.
+ parent.skipBytes(1); // DecoderSpecificInfo tag
+ int initializationDataSize = parseExpandableClassSize(parent);
+ byte[] initializationData = new byte[initializationDataSize];
+ parent.readBytes(initializationData, 0, initializationDataSize);
+ return Pair.create(mimeType, initializationData);
+ }
+
+ /**
+ * Parses encryption data from an audio/video sample entry, returning a pair consisting of the
+ * unencrypted atom type and a {@link TrackEncryptionBox}. Null is returned if no common
+ * encryption sinf atom was present.
+ */
+ private static Pair<Integer, TrackEncryptionBox> parseSampleEntryEncryptionData(
+ ParsableByteArray parent, int position, int size) {
+ int childPosition = parent.getPosition();
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_sinf) {
+ Pair<Integer, TrackEncryptionBox> result = parseCommonEncryptionSinfFromParent(parent,
+ childPosition, childAtomSize);
+ if (result != null) {
+ return result;
+ }
+ }
+ childPosition += childAtomSize;
+ }
+ return null;
+ }
+
+ /* package */ static Pair<Integer, TrackEncryptionBox> parseCommonEncryptionSinfFromParent(
+ ParsableByteArray parent, int position, int size) {
+ int childPosition = position + Atom.HEADER_SIZE;
+ int schemeInformationBoxPosition = C.POSITION_UNSET;
+ int schemeInformationBoxSize = 0;
+ String schemeType = null;
+ Integer dataFormat = null;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_frma) {
+ dataFormat = parent.readInt();
+ } else if (childAtomType == Atom.TYPE_schm) {
+ parent.skipBytes(4);
+ // Common encryption scheme_type values are defined in ISO/IEC 23001-7:2016, section 4.1.
+ schemeType = parent.readString(4);
+ } else if (childAtomType == Atom.TYPE_schi) {
+ schemeInformationBoxPosition = childPosition;
+ schemeInformationBoxSize = childAtomSize;
+ }
+ childPosition += childAtomSize;
+ }
+
+ if (C.CENC_TYPE_cenc.equals(schemeType) || C.CENC_TYPE_cbc1.equals(schemeType)
+ || C.CENC_TYPE_cens.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType)) {
+ Assertions.checkArgument(dataFormat != null, "frma atom is mandatory");
+ Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET,
+ "schi atom is mandatory");
+ TrackEncryptionBox encryptionBox = parseSchiFromParent(parent, schemeInformationBoxPosition,
+ schemeInformationBoxSize, schemeType);
+ Assertions.checkArgument(encryptionBox != null, "tenc atom is mandatory");
+ return Pair.create(dataFormat, encryptionBox);
+ } else {
+ return null;
+ }
+ }
+
+ private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,
+ int size, String schemeType) {
+ int childPosition = position + Atom.HEADER_SIZE;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_tenc) {
+ int fullAtom = parent.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ parent.skipBytes(1); // reserved = 0.
+ int defaultCryptByteBlock = 0;
+ int defaultSkipByteBlock = 0;
+ if (version == 0) {
+ parent.skipBytes(1); // reserved = 0.
+ } else /* version 1 or greater */ {
+ int patternByte = parent.readUnsignedByte();
+ defaultCryptByteBlock = (patternByte & 0xF0) >> 4;
+ defaultSkipByteBlock = patternByte & 0x0F;
+ }
+ boolean defaultIsProtected = parent.readUnsignedByte() == 1;
+ int defaultPerSampleIvSize = parent.readUnsignedByte();
+ byte[] defaultKeyId = new byte[16];
+ parent.readBytes(defaultKeyId, 0, defaultKeyId.length);
+ byte[] constantIv = null;
+ if (defaultIsProtected && defaultPerSampleIvSize == 0) {
+ int constantIvSize = parent.readUnsignedByte();
+ constantIv = new byte[constantIvSize];
+ parent.readBytes(constantIv, 0, constantIvSize);
+ }
+ return new TrackEncryptionBox(defaultIsProtected, schemeType, defaultPerSampleIvSize,
+ defaultKeyId, defaultCryptByteBlock, defaultSkipByteBlock, constantIv);
+ }
+ childPosition += childAtomSize;
+ }
+ return null;
+ }
+
+ /**
+ * Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media.
+ */
+ private static byte[] parseProjFromParent(ParsableByteArray parent, int position, int size) {
+ int childPosition = position + Atom.HEADER_SIZE;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_proj) {
+ return Arrays.copyOfRange(parent.data, childPosition, childPosition + childAtomSize);
+ }
+ childPosition += childAtomSize;
+ }
+ return null;
+ }
+
+ /**
+ * 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;
+ while ((currentByte & 0x80) == 0x80) {
+ currentByte = data.readUnsignedByte();
+ size = (size << 7) | (currentByte & 0x7F);
+ }
+ return size;
+ }
+
+ /** Returns whether it's possible to apply the specified edit using gapless playback info. */
+ private static boolean canApplyEditWithGaplessInfo(
+ long[] timestamps, long duration, long editStartTime, long editEndTime) {
+ int lastIndex = timestamps.length - 1;
+ int latestDelayIndex = Util.constrainValue(MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex);
+ int earliestPaddingIndex =
+ Util.constrainValue(timestamps.length - MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex);
+ return timestamps[0] <= editStartTime
+ && editStartTime < timestamps[latestDelayIndex]
+ && timestamps[earliestPaddingIndex] < editEndTime
+ && editEndTime <= duration;
+ }
+
+ private AtomParsers() {
+ // Prevent instantiation.
+ }
+
+ private static final class ChunkIterator {
+
+ public final int length;
+
+ public int index;
+ public int numSamples;
+ public long offset;
+
+ private final boolean chunkOffsetsAreLongs;
+ private final ParsableByteArray chunkOffsets;
+ private final ParsableByteArray stsc;
+
+ private int nextSamplesPerChunkChangeIndex;
+ private int remainingSamplesPerChunkChanges;
+
+ public ChunkIterator(ParsableByteArray stsc, ParsableByteArray chunkOffsets,
+ boolean chunkOffsetsAreLongs) {
+ this.stsc = stsc;
+ this.chunkOffsets = chunkOffsets;
+ this.chunkOffsetsAreLongs = chunkOffsetsAreLongs;
+ chunkOffsets.setPosition(Atom.FULL_HEADER_SIZE);
+ length = chunkOffsets.readUnsignedIntToInt();
+ stsc.setPosition(Atom.FULL_HEADER_SIZE);
+ remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt();
+ Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1");
+ index = -1;
+ }
+
+ public boolean moveNext() {
+ if (++index == length) {
+ return false;
+ }
+ offset = chunkOffsetsAreLongs ? chunkOffsets.readUnsignedLongToLong()
+ : chunkOffsets.readUnsignedInt();
+ if (index == nextSamplesPerChunkChangeIndex) {
+ numSamples = stsc.readUnsignedIntToInt();
+ stsc.skipBytes(4); // Skip sample_description_index
+ nextSamplesPerChunkChangeIndex = --remainingSamplesPerChunkChanges > 0
+ ? (stsc.readUnsignedIntToInt() - 1) : C.INDEX_UNSET;
+ }
+ return true;
+ }
+
+ }
+
+ /**
+ * Holds data parsed from a tkhd atom.
+ */
+ private static final class TkhdData {
+
+ private final int id;
+ private final long duration;
+ private final int rotationDegrees;
+
+ public TkhdData(int id, long duration, int rotationDegrees) {
+ this.id = id;
+ this.duration = duration;
+ this.rotationDegrees = rotationDegrees;
+ }
+
+ }
+
+ /**
+ * Holds data parsed from an stsd atom and its children.
+ */
+ private static final class StsdData {
+
+ public static final int STSD_HEADER_SIZE = 8;
+
+ public final TrackEncryptionBox[] trackEncryptionBoxes;
+
+ public Format format;
+ public int nalUnitLengthFieldLength;
+ @Track.Transformation
+ public int requiredSampleTransformation;
+
+ public StsdData(int numberOfEntries) {
+ trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
+ 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/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java
new file mode 100644
index 0000000000..0942673435
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java
@@ -0,0 +1,32 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+/* package */ final class DefaultSampleValues {
+
+ public final int sampleDescriptionIndex;
+ public final int duration;
+ public final int size;
+ public final int flags;
+
+ public DefaultSampleValues(int sampleDescriptionIndex, int duration, int size, int flags) {
+ this.sampleDescriptionIndex = sampleDescriptionIndex;
+ this.duration = duration;
+ this.size = size;
+ this.flags = flags;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java
new file mode 100644
index 0000000000..78d30ba582
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java
@@ -0,0 +1,114 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/**
+ * Rechunks fixed sample size media in which every sample is a key frame (e.g. uncompressed audio).
+ */
+/* package */ final class FixedSampleSizeRechunker {
+
+ /**
+ * The result of a rechunking operation.
+ */
+ public static final class Results {
+
+ public final long[] offsets;
+ public final int[] sizes;
+ public final int maximumSize;
+ public final long[] timestamps;
+ public final int[] flags;
+ public final long duration;
+
+ private Results(
+ long[] offsets,
+ int[] sizes,
+ int maximumSize,
+ long[] timestamps,
+ int[] flags,
+ long duration) {
+ this.offsets = offsets;
+ this.sizes = sizes;
+ this.maximumSize = maximumSize;
+ this.timestamps = timestamps;
+ this.flags = flags;
+ this.duration = duration;
+ }
+
+ }
+
+ /**
+ * Maximum number of bytes for each buffer in rechunked output.
+ */
+ private static final int MAX_SAMPLE_SIZE = 8 * 1024;
+
+ /**
+ * Rechunk the given fixed sample size input to produce a new sequence of samples.
+ *
+ * @param fixedSampleSize Size in bytes of each sample.
+ * @param chunkOffsets Chunk offsets in the MP4 stream to rechunk.
+ * @param chunkSampleCounts Sample counts for each of the MP4 stream's chunks.
+ * @param timestampDeltaInTimeUnits Timestamp delta between each sample in time units.
+ */
+ public static Results rechunk(int fixedSampleSize, long[] chunkOffsets, int[] chunkSampleCounts,
+ long timestampDeltaInTimeUnits) {
+ int maxSampleCount = MAX_SAMPLE_SIZE / fixedSampleSize;
+
+ // Count the number of new, rechunked buffers.
+ int rechunkedSampleCount = 0;
+ for (int chunkSampleCount : chunkSampleCounts) {
+ rechunkedSampleCount += Util.ceilDivide(chunkSampleCount, maxSampleCount);
+ }
+
+ long[] offsets = new long[rechunkedSampleCount];
+ int[] sizes = new int[rechunkedSampleCount];
+ int maximumSize = 0;
+ long[] timestamps = new long[rechunkedSampleCount];
+ int[] flags = new int[rechunkedSampleCount];
+
+ int originalSampleIndex = 0;
+ int newSampleIndex = 0;
+ for (int chunkIndex = 0; chunkIndex < chunkSampleCounts.length; chunkIndex++) {
+ int chunkSamplesRemaining = chunkSampleCounts[chunkIndex];
+ long sampleOffset = chunkOffsets[chunkIndex];
+
+ while (chunkSamplesRemaining > 0) {
+ int bufferSampleCount = Math.min(maxSampleCount, chunkSamplesRemaining);
+
+ offsets[newSampleIndex] = sampleOffset;
+ sizes[newSampleIndex] = fixedSampleSize * bufferSampleCount;
+ maximumSize = Math.max(maximumSize, sizes[newSampleIndex]);
+ timestamps[newSampleIndex] = (timestampDeltaInTimeUnits * originalSampleIndex);
+ flags[newSampleIndex] = C.BUFFER_FLAG_KEY_FRAME;
+
+ sampleOffset += sizes[newSampleIndex];
+ originalSampleIndex += bufferSampleCount;
+
+ chunkSamplesRemaining -= bufferSampleCount;
+ newSampleIndex++;
+ }
+ }
+ long duration = timestampDeltaInTimeUnits * originalSampleIndex;
+
+ return new Results(offsets, sizes, maximumSize, timestamps, flags, duration);
+ }
+
+ private FixedSampleSizeRechunker() {
+ // Prevent instantiation.
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
new file mode 100644
index 0000000000..291a9ade27
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
@@ -0,0 +1,1660 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import android.util.Pair;
+import android.util.SparseArray;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ChunkIndex;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessage;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.CeaUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import org.mozilla.thirdparty.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;
+
+/** 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 =
+ Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE);
+
+ // 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;
+ private ParsableByteArray atomData;
+ private long endOfMdatPosition;
+ private int pendingMetadataSampleBytes;
+ private long pendingSeekTimeUs;
+
+ private long durationUs;
+ private long segmentIndexEarliestPresentationTimeUs;
+ private TrackBundle currentTrackBundle;
+ private int sampleSize;
+ private int sampleBytesWritten;
+ private int sampleCurrentNalBytesRemaining;
+ private boolean processSeiNalUnitPayload;
+
+ // Extractor output.
+ private 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, InterruptedException {
+ 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, InterruptedException {
+ 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, InterruptedException {
+ 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, InterruptedException {
+ 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) {
+ 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);
+ TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray);
+ if (trackBundle == null) {
+ return;
+ }
+
+ TrackFragment fragment = trackBundle.fragment;
+ long decodeTime = fragment.nextFragmentDecodeTime;
+ trackBundle.reset();
+
+ 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);
+
+ TrackEncryptionBox encryptionBox = trackBundle.track
+ .getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);
+
+ LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
+ if (saiz != null) {
+ parseSaiz(encryptionBox, saiz.data, fragment);
+ }
+
+ LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio);
+ if (saio != null) {
+ parseSaio(saio.data, fragment);
+ }
+
+ LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc);
+ if (senc != null) {
+ parseSenc(senc.data, fragment);
+ }
+
+ LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp);
+ 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}.
+ */
+ 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();
+ 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, 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, InterruptedException {
+ 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.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private boolean readSample(ExtractorInput input) throws IOException, InterruptedException {
+ if (parserState == STATE_READING_SAMPLE_START) {
+ if (currentTrackBundle == null) {
+ 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.
+ */
+ 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) {
+ 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;
+ 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) {
+ TrackEncryptionBox encryptionBox =
+ track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);
+ String schemeType = encryptionBox != null ? encryptionBox.schemeType : null;
+ output.format(track.format.copyWithDrmInitData(drmInitData.copyWithSchemeType(schemeType)));
+ }
+
+ /** 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() {
+ 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());
+ }
+ }
+
+ private TrackEncryptionBox getEncryptionBoxIfEncrypted() {
+ int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex;
+ TrackEncryptionBox encryptionBox =
+ fragment.trackEncryptionBox != null
+ ? fragment.trackEncryptionBox
+ : track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex);
+ return encryptionBox != null && encryptionBox.isEncrypted ? encryptionBox : null;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java
new file mode 100644
index 0000000000..7040df6425
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/**
+ * Stores extensible metadata with handler type 'mdta'. See also the QuickTime File Format
+ * Specification.
+ */
+public final class MdtaMetadataEntry implements Metadata.Entry {
+
+ /** The metadata key name. */
+ public final String key;
+ /** The payload. The interpretation of the value depends on {@link #typeIndicator}. */
+ public final byte[] value;
+ /** The four byte locale indicator. */
+ public final int localeIndicator;
+ /** The four byte type indicator. */
+ public final int typeIndicator;
+
+ /** Creates a new metadata entry for the specified metadata key/value. */
+ public MdtaMetadataEntry(String key, byte[] value, int localeIndicator, int typeIndicator) {
+ this.key = key;
+ this.value = value;
+ this.localeIndicator = localeIndicator;
+ this.typeIndicator = typeIndicator;
+ }
+
+ private MdtaMetadataEntry(Parcel in) {
+ key = Util.castNonNull(in.readString());
+ value = new byte[in.readInt()];
+ in.readByteArray(value);
+ localeIndicator = in.readInt();
+ typeIndicator = in.readInt();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ MdtaMetadataEntry other = (MdtaMetadataEntry) obj;
+ return key.equals(other.key)
+ && Arrays.equals(value, other.value)
+ && localeIndicator == other.localeIndicator
+ && typeIndicator == other.typeIndicator;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + key.hashCode();
+ result = 31 * result + Arrays.hashCode(value);
+ result = 31 * result + localeIndicator;
+ result = 31 * result + typeIndicator;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "mdta: key=" + key;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(key);
+ dest.writeInt(value.length);
+ dest.writeByteArray(value);
+ dest.writeInt(localeIndicator);
+ dest.writeInt(typeIndicator);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<MdtaMetadataEntry> CREATOR =
+ new Parcelable.Creator<MdtaMetadataEntry>() {
+
+ @Override
+ public MdtaMetadataEntry createFromParcel(Parcel in) {
+ return new MdtaMetadataEntry(in);
+ }
+
+ @Override
+ public MdtaMetadataEntry[] newArray(int size) {
+ return new MdtaMetadataEntry[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
new file mode 100644
index 0000000000..7d4de0e498
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
@@ -0,0 +1,588 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.ApicFrame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.CommentFrame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Frame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.InternalFrame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.nio.ByteBuffer;
+
+/** Utilities for handling metadata in MP4. */
+/* package */ final class MetadataUtil {
+
+ private static final String TAG = "MetadataUtil";
+
+ // Codes that start with the copyright character (omitted) and have equivalent ID3 frames.
+ private static final int SHORT_TYPE_NAME_1 = 0x006e616d;
+ private static final int SHORT_TYPE_NAME_2 = 0x0074726b;
+ private static final int SHORT_TYPE_COMMENT = 0x00636d74;
+ private static final int SHORT_TYPE_YEAR = 0x00646179;
+ private static final int SHORT_TYPE_ARTIST = 0x00415254;
+ private static final int SHORT_TYPE_ENCODER = 0x00746f6f;
+ private static final int SHORT_TYPE_ALBUM = 0x00616c62;
+ private static final int SHORT_TYPE_COMPOSER_1 = 0x00636f6d;
+ private static final int SHORT_TYPE_COMPOSER_2 = 0x00777274;
+ private static final int SHORT_TYPE_LYRICS = 0x006c7972;
+ private static final int SHORT_TYPE_GENRE = 0x0067656e;
+
+ // Codes that have equivalent ID3 frames.
+ private static final int TYPE_COVER_ART = 0x636f7672;
+ private static final int TYPE_GENRE = 0x676e7265;
+ private static final int TYPE_GROUPING = 0x00677270;
+ private static final int TYPE_DISK_NUMBER = 0x6469736b;
+ private static final int TYPE_TRACK_NUMBER = 0x74726b6e;
+ private static final int TYPE_TEMPO = 0x746d706f;
+ private static final int TYPE_COMPILATION = 0x6370696c;
+ private static final int TYPE_ALBUM_ARTIST = 0x61415254;
+ private static final int TYPE_SORT_TRACK_NAME = 0x736f6e6d;
+ private static final int TYPE_SORT_ALBUM = 0x736f616c;
+ private static final int TYPE_SORT_ARTIST = 0x736f6172;
+ private static final int TYPE_SORT_ALBUM_ARTIST = 0x736f6161;
+ private static final int TYPE_SORT_COMPOSER = 0x736f636f;
+
+ // Types that do not have equivalent ID3 frames.
+ private static final int TYPE_RATING = 0x72746e67;
+ private static final int TYPE_GAPLESS_ALBUM = 0x70676170;
+ private static final int TYPE_TV_SORT_SHOW = 0x736f736e;
+ private static final int TYPE_TV_SHOW = 0x74767368;
+
+ // Type for items that are intended for internal use by the player.
+ private static final int TYPE_INTERNAL = 0x2d2d2d2d;
+
+ private static final int PICTURE_TYPE_FRONT_COVER = 3;
+
+ // Standard genres.
+ @VisibleForTesting
+ /* package */ static final String[] STANDARD_GENRES =
+ new String[] {
+ // These are the official ID3v1 genres.
+ "Blues",
+ "Classic Rock",
+ "Country",
+ "Dance",
+ "Disco",
+ "Funk",
+ "Grunge",
+ "Hip-Hop",
+ "Jazz",
+ "Metal",
+ "New Age",
+ "Oldies",
+ "Other",
+ "Pop",
+ "R&B",
+ "Rap",
+ "Reggae",
+ "Rock",
+ "Techno",
+ "Industrial",
+ "Alternative",
+ "Ska",
+ "Death Metal",
+ "Pranks",
+ "Soundtrack",
+ "Euro-Techno",
+ "Ambient",
+ "Trip-Hop",
+ "Vocal",
+ "Jazz+Funk",
+ "Fusion",
+ "Trance",
+ "Classical",
+ "Instrumental",
+ "Acid",
+ "House",
+ "Game",
+ "Sound Clip",
+ "Gospel",
+ "Noise",
+ "AlternRock",
+ "Bass",
+ "Soul",
+ "Punk",
+ "Space",
+ "Meditative",
+ "Instrumental Pop",
+ "Instrumental Rock",
+ "Ethnic",
+ "Gothic",
+ "Darkwave",
+ "Techno-Industrial",
+ "Electronic",
+ "Pop-Folk",
+ "Eurodance",
+ "Dream",
+ "Southern Rock",
+ "Comedy",
+ "Cult",
+ "Gangsta",
+ "Top 40",
+ "Christian Rap",
+ "Pop/Funk",
+ "Jungle",
+ "Native American",
+ "Cabaret",
+ "New Wave",
+ "Psychadelic",
+ "Rave",
+ "Showtunes",
+ "Trailer",
+ "Lo-Fi",
+ "Tribal",
+ "Acid Punk",
+ "Acid Jazz",
+ "Polka",
+ "Retro",
+ "Musical",
+ "Rock & Roll",
+ "Hard Rock",
+ // Genres made up by the authors of Winamp (v1.91) and later added to the ID3 spec.
+ "Folk",
+ "Folk-Rock",
+ "National Folk",
+ "Swing",
+ "Fast Fusion",
+ "Bebob",
+ "Latin",
+ "Revival",
+ "Celtic",
+ "Bluegrass",
+ "Avantgarde",
+ "Gothic Rock",
+ "Progressive Rock",
+ "Psychedelic Rock",
+ "Symphonic Rock",
+ "Slow Rock",
+ "Big Band",
+ "Chorus",
+ "Easy Listening",
+ "Acoustic",
+ "Humour",
+ "Speech",
+ "Chanson",
+ "Opera",
+ "Chamber Music",
+ "Sonata",
+ "Symphony",
+ "Booty Bass",
+ "Primus",
+ "Porn Groove",
+ "Satire",
+ "Slow Jam",
+ "Club",
+ "Tango",
+ "Samba",
+ "Folklore",
+ "Ballad",
+ "Power Ballad",
+ "Rhythmic Soul",
+ "Freestyle",
+ "Duet",
+ "Punk Rock",
+ "Drum Solo",
+ "A capella",
+ "Euro-House",
+ "Dance Hall",
+ // Genres made up by the authors of Winamp (v1.91) but have not been added to the ID3 spec.
+ "Goa",
+ "Drum & Bass",
+ "Club-House",
+ "Hardcore",
+ "Terror",
+ "Indie",
+ "BritPop",
+ "Afro-Punk",
+ "Polsk Punk",
+ "Beat",
+ "Christian Gangsta Rap",
+ "Heavy Metal",
+ "Black Metal",
+ "Crossover",
+ "Contemporary Christian",
+ "Christian Rock",
+ "Merengue",
+ "Salsa",
+ "Thrash Metal",
+ "Anime",
+ "Jpop",
+ "Synthpop",
+ // Genres made up by the authors of Winamp (v5.6) but have not been added to the ID3 spec.
+ "Abstract",
+ "Art Rock",
+ "Baroque",
+ "Bhangra",
+ "Big beat",
+ "Breakbeat",
+ "Chillout",
+ "Downtempo",
+ "Dub",
+ "EBM",
+ "Eclectic",
+ "Electro",
+ "Electroclash",
+ "Emo",
+ "Experimental",
+ "Garage",
+ "Global",
+ "IDM",
+ "Illbient",
+ "Industro-Goth",
+ "Jam Band",
+ "Krautrock",
+ "Leftfield",
+ "Lounge",
+ "Math Rock",
+ "New Romantic",
+ "Nu-Breakz",
+ "Post-Punk",
+ "Post-Rock",
+ "Psytrance",
+ "Shoegaze",
+ "Space Rock",
+ "Trop Rock",
+ "World Music",
+ "Neoclassical",
+ "Audiobook",
+ "Audio theatre",
+ "Neue Deutsche Welle",
+ "Podcast",
+ "Indie-Rock",
+ "G-Funk",
+ "Dubstep",
+ "Garage Rock",
+ "Psybient"
+ };
+
+ private static final String LANGUAGE_UNDEFINED = "und";
+
+ private static final int TYPE_TOP_BYTE_COPYRIGHT = 0xA9;
+ private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \uFFFD.
+
+ private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps";
+ private static final int MDTA_TYPE_INDICATOR_FLOAT = 23;
+
+ private MetadataUtil() {}
+
+ /**
+ * Returns a {@link Format} that is the same as the input format but includes information from the
+ * specified sources of metadata.
+ */
+ public static Format getFormatWithMetadata(
+ int trackType,
+ Format format,
+ @Nullable Metadata udtaMetadata,
+ @Nullable Metadata mdtaMetadata,
+ GaplessInfoHolder gaplessInfoHolder) {
+ if (trackType == C.TRACK_TYPE_AUDIO) {
+ if (gaplessInfoHolder.hasGaplessInfo()) {
+ format =
+ format.copyWithGaplessInfo(
+ gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding);
+ }
+ // We assume all udta metadata is associated with the audio track.
+ if (udtaMetadata != null) {
+ format = format.copyWithMetadata(udtaMetadata);
+ }
+ } else if (trackType == C.TRACK_TYPE_VIDEO && mdtaMetadata != null) {
+ // Populate only metadata keys that are known to be specific to video.
+ for (int i = 0; i < mdtaMetadata.length(); i++) {
+ Metadata.Entry entry = mdtaMetadata.get(i);
+ if (entry instanceof MdtaMetadataEntry) {
+ MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry;
+ if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key)
+ && mdtaMetadataEntry.typeIndicator == MDTA_TYPE_INDICATOR_FLOAT) {
+ try {
+ float fps = ByteBuffer.wrap(mdtaMetadataEntry.value).asFloatBuffer().get();
+ format = format.copyWithFrameRate(fps);
+ format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry));
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring invalid framerate");
+ }
+ }
+ }
+ }
+ }
+ return format;
+ }
+
+ /**
+ * Parses a single userdata ilst element from a {@link ParsableByteArray}. The element is read
+ * starting from the current position of the {@link ParsableByteArray}, and the position is
+ * advanced by the size of the element. The position is advanced even if the element's type is
+ * unrecognized.
+ *
+ * @param ilst Holds the data to be parsed.
+ * @return The parsed element, or null if the element's type was not recognized.
+ */
+ @Nullable
+ public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
+ int position = ilst.getPosition();
+ int endPosition = position + ilst.readInt();
+ int type = ilst.readInt();
+ int typeTopByte = (type >> 24) & 0xFF;
+ try {
+ if (typeTopByte == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) {
+ int shortType = type & 0x00FFFFFF;
+ if (shortType == SHORT_TYPE_COMMENT) {
+ return parseCommentAttribute(type, ilst);
+ } else if (shortType == SHORT_TYPE_NAME_1 || shortType == SHORT_TYPE_NAME_2) {
+ return parseTextAttribute(type, "TIT2", ilst);
+ } else if (shortType == SHORT_TYPE_COMPOSER_1 || shortType == SHORT_TYPE_COMPOSER_2) {
+ return parseTextAttribute(type, "TCOM", ilst);
+ } else if (shortType == SHORT_TYPE_YEAR) {
+ return parseTextAttribute(type, "TDRC", ilst);
+ } else if (shortType == SHORT_TYPE_ARTIST) {
+ return parseTextAttribute(type, "TPE1", ilst);
+ } else if (shortType == SHORT_TYPE_ENCODER) {
+ return parseTextAttribute(type, "TSSE", ilst);
+ } else if (shortType == SHORT_TYPE_ALBUM) {
+ return parseTextAttribute(type, "TALB", ilst);
+ } else if (shortType == SHORT_TYPE_LYRICS) {
+ return parseTextAttribute(type, "USLT", ilst);
+ } else if (shortType == SHORT_TYPE_GENRE) {
+ return parseTextAttribute(type, "TCON", ilst);
+ } else if (shortType == TYPE_GROUPING) {
+ return parseTextAttribute(type, "TIT1", ilst);
+ }
+ } else if (type == TYPE_GENRE) {
+ return parseStandardGenreAttribute(ilst);
+ } else if (type == TYPE_DISK_NUMBER) {
+ return parseIndexAndCountAttribute(type, "TPOS", ilst);
+ } else if (type == TYPE_TRACK_NUMBER) {
+ return parseIndexAndCountAttribute(type, "TRCK", ilst);
+ } else if (type == TYPE_TEMPO) {
+ return parseUint8Attribute(type, "TBPM", ilst, true, false);
+ } else if (type == TYPE_COMPILATION) {
+ return parseUint8Attribute(type, "TCMP", ilst, true, true);
+ } else if (type == TYPE_COVER_ART) {
+ return parseCoverArt(ilst);
+ } else if (type == TYPE_ALBUM_ARTIST) {
+ return parseTextAttribute(type, "TPE2", ilst);
+ } else if (type == TYPE_SORT_TRACK_NAME) {
+ return parseTextAttribute(type, "TSOT", ilst);
+ } else if (type == TYPE_SORT_ALBUM) {
+ return parseTextAttribute(type, "TSO2", ilst);
+ } else if (type == TYPE_SORT_ARTIST) {
+ return parseTextAttribute(type, "TSOA", ilst);
+ } else if (type == TYPE_SORT_ALBUM_ARTIST) {
+ return parseTextAttribute(type, "TSOP", ilst);
+ } else if (type == TYPE_SORT_COMPOSER) {
+ return parseTextAttribute(type, "TSOC", ilst);
+ } else if (type == TYPE_RATING) {
+ return parseUint8Attribute(type, "ITUNESADVISORY", ilst, false, false);
+ } else if (type == TYPE_GAPLESS_ALBUM) {
+ return parseUint8Attribute(type, "ITUNESGAPLESS", ilst, false, true);
+ } else if (type == TYPE_TV_SORT_SHOW) {
+ return parseTextAttribute(type, "TVSHOWSORT", ilst);
+ } else if (type == TYPE_TV_SHOW) {
+ return parseTextAttribute(type, "TVSHOW", ilst);
+ } else if (type == TYPE_INTERNAL) {
+ return parseInternalAttribute(ilst, endPosition);
+ }
+ Log.d(TAG, "Skipped unknown metadata entry: " + Atom.getAtomTypeString(type));
+ return null;
+ } finally {
+ ilst.setPosition(endPosition);
+ }
+ }
+
+ /**
+ * Parses an 'mdta' metadata entry starting at the current position in an ilst box.
+ *
+ * @param ilst The ilst box.
+ * @param endPosition The end position of the entry in the ilst box.
+ * @param key The mdta metadata entry key for the entry.
+ * @return The parsed element, or null if the entry wasn't recognized.
+ */
+ @Nullable
+ public static MdtaMetadataEntry parseMdtaMetadataEntryFromIlst(
+ ParsableByteArray ilst, int endPosition, String key) {
+ int atomPosition;
+ while ((atomPosition = ilst.getPosition()) < endPosition) {
+ int atomSize = ilst.readInt();
+ int atomType = ilst.readInt();
+ if (atomType == Atom.TYPE_data) {
+ int typeIndicator = ilst.readInt();
+ int localeIndicator = ilst.readInt();
+ int dataSize = atomSize - 16;
+ byte[] value = new byte[dataSize];
+ ilst.readBytes(value, 0, dataSize);
+ return new MdtaMetadataEntry(key, value, localeIndicator, typeIndicator);
+ }
+ ilst.setPosition(atomPosition + atomSize);
+ }
+ return null;
+ }
+
+ @Nullable
+ private static TextInformationFrame parseTextAttribute(
+ int type, String id, ParsableByteArray data) {
+ int atomSize = data.readInt();
+ int atomType = data.readInt();
+ if (atomType == Atom.TYPE_data) {
+ data.skipBytes(8); // version (1), flags (3), empty (4)
+ String value = data.readNullTerminatedString(atomSize - 16);
+ return new TextInformationFrame(id, /* description= */ null, value);
+ }
+ Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type));
+ return null;
+ }
+
+ @Nullable
+ private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {
+ int atomSize = data.readInt();
+ int atomType = data.readInt();
+ if (atomType == Atom.TYPE_data) {
+ data.skipBytes(8); // version (1), flags (3), empty (4)
+ String value = data.readNullTerminatedString(atomSize - 16);
+ return new CommentFrame(LANGUAGE_UNDEFINED, value, value);
+ }
+ Log.w(TAG, "Failed to parse comment attribute: " + Atom.getAtomTypeString(type));
+ return null;
+ }
+
+ @Nullable
+ private static Id3Frame parseUint8Attribute(
+ int type,
+ String id,
+ ParsableByteArray data,
+ boolean isTextInformationFrame,
+ boolean isBoolean) {
+ int value = parseUint8AttributeValue(data);
+ if (isBoolean) {
+ value = Math.min(1, value);
+ }
+ if (value >= 0) {
+ return isTextInformationFrame
+ ? new TextInformationFrame(id, /* description= */ null, Integer.toString(value))
+ : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value));
+ }
+ Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type));
+ return null;
+ }
+
+ @Nullable
+ private static TextInformationFrame parseIndexAndCountAttribute(
+ int type, String attributeName, ParsableByteArray data) {
+ int atomSize = data.readInt();
+ int atomType = data.readInt();
+ if (atomType == Atom.TYPE_data && atomSize >= 22) {
+ data.skipBytes(10); // version (1), flags (3), empty (4), empty (2)
+ int index = data.readUnsignedShort();
+ if (index > 0) {
+ String value = "" + index;
+ int count = data.readUnsignedShort();
+ if (count > 0) {
+ value += "/" + count;
+ }
+ return new TextInformationFrame(attributeName, /* description= */ null, value);
+ }
+ }
+ Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type));
+ return null;
+ }
+
+ @Nullable
+ private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) {
+ int genreCode = parseUint8AttributeValue(data);
+ String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
+ ? STANDARD_GENRES[genreCode - 1] : null;
+ if (genreString != null) {
+ return new TextInformationFrame("TCON", /* description= */ null, genreString);
+ }
+ Log.w(TAG, "Failed to parse standard genre code");
+ return null;
+ }
+
+ @Nullable
+ private static ApicFrame parseCoverArt(ParsableByteArray data) {
+ int atomSize = data.readInt();
+ int atomType = data.readInt();
+ if (atomType == Atom.TYPE_data) {
+ int fullVersionInt = data.readInt();
+ int flags = Atom.parseFullAtomFlags(fullVersionInt);
+ String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null;
+ if (mimeType == null) {
+ Log.w(TAG, "Unrecognized cover art flags: " + flags);
+ return null;
+ }
+ data.skipBytes(4); // empty (4)
+ byte[] pictureData = new byte[atomSize - 16];
+ data.readBytes(pictureData, 0, pictureData.length);
+ return new ApicFrame(
+ mimeType,
+ /* description= */ null,
+ /* pictureType= */ PICTURE_TYPE_FRONT_COVER,
+ pictureData);
+ }
+ Log.w(TAG, "Failed to parse cover art attribute");
+ return null;
+ }
+
+ @Nullable
+ private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) {
+ String domain = null;
+ String name = null;
+ int dataAtomPosition = -1;
+ int dataAtomSize = -1;
+ while (data.getPosition() < endPosition) {
+ int atomPosition = data.getPosition();
+ int atomSize = data.readInt();
+ int atomType = data.readInt();
+ data.skipBytes(4); // version (1), flags (3)
+ if (atomType == Atom.TYPE_mean) {
+ domain = data.readNullTerminatedString(atomSize - 12);
+ } else if (atomType == Atom.TYPE_name) {
+ name = data.readNullTerminatedString(atomSize - 12);
+ } else {
+ if (atomType == Atom.TYPE_data) {
+ dataAtomPosition = atomPosition;
+ dataAtomSize = atomSize;
+ }
+ data.skipBytes(atomSize - 12);
+ }
+ }
+ if (domain == null || name == null || dataAtomPosition == -1) {
+ return null;
+ }
+ data.setPosition(dataAtomPosition);
+ data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4)
+ String value = data.readNullTerminatedString(dataAtomSize - 16);
+ return new InternalFrame(domain, name, value);
+ }
+
+ private static int parseUint8AttributeValue(ParsableByteArray data) {
+ data.skipBytes(4); // atomSize
+ int atomType = data.readInt();
+ if (atomType == Atom.TYPE_data) {
+ data.skipBytes(8); // version (1), flags (3), empty (4)
+ return data.readUnsignedByte();
+ }
+ Log.w(TAG, "Failed to parse uint8 attribute value");
+ return -1;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
new file mode 100644
index 0000000000..254cad1eb1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
@@ -0,0 +1,824 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+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.List;
+
+/**
+ * Extracts data from the MP4 container format.
+ */
+public final class Mp4Extractor implements Extractor, SeekMap {
+
+ /** Factory for {@link Mp4Extractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp4Extractor()};
+
+ /**
+ * Flags controlling the behavior of the extractor. Possible flag value is {@link
+ * #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {FLAG_WORKAROUND_IGNORE_EDIT_LISTS})
+ public @interface Flags {}
+ /**
+ * Flag to ignore any edit lists in the stream.
+ */
+ public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1;
+
+ /** Parser states. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATE_READING_ATOM_HEADER, STATE_READING_ATOM_PAYLOAD, STATE_READING_SAMPLE})
+ private @interface State {}
+
+ private static final int STATE_READING_ATOM_HEADER = 0;
+ private static final int STATE_READING_ATOM_PAYLOAD = 1;
+ private static final int STATE_READING_SAMPLE = 2;
+
+ /** Brand stored in the ftyp atom for QuickTime media. */
+ private static final int BRAND_QUICKTIME = 0x71742020;
+
+ /**
+ * When seeking within the source, if the offset is greater than or equal to this value (or the
+ * offset is negative), the source will be reloaded.
+ */
+ private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024;
+
+ /**
+ * For poorly interleaved streams, the maximum byte difference one track is allowed to be read
+ * ahead before the source will be reloaded at a new position to read another track.
+ */
+ private static final long MAXIMUM_READ_AHEAD_BYTES_STREAM = 10 * 1024 * 1024;
+
+ private final @Flags int flags;
+
+ // Temporary arrays.
+ private final ParsableByteArray nalStartCode;
+ private final ParsableByteArray nalLength;
+ private final ParsableByteArray scratch;
+
+ private final ParsableByteArray atomHeader;
+ private final ArrayDeque<ContainerAtom> containerAtoms;
+
+ @State private int parserState;
+ private int atomType;
+ private long atomSize;
+ private int atomHeaderBytesRead;
+ private ParsableByteArray atomData;
+
+ private int sampleTrackIndex;
+ private int sampleBytesRead;
+ private int sampleBytesWritten;
+ private int sampleCurrentNalBytesRemaining;
+
+ // Extractor outputs.
+ private ExtractorOutput extractorOutput;
+ private Mp4Track[] tracks;
+ private long[][] accumulatedSampleSizes;
+ private int firstVideoTrackIndex;
+ private long durationUs;
+ private boolean isQuickTime;
+
+ /**
+ * Creates a new extractor for unfragmented MP4 streams.
+ */
+ public Mp4Extractor() {
+ this(0);
+ }
+
+ /**
+ * Creates a new extractor for unfragmented MP4 streams, using the specified flags to control the
+ * extractor's behavior.
+ *
+ * @param flags Flags that control the extractor's behavior.
+ */
+ public Mp4Extractor(@Flags int flags) {
+ this.flags = flags;
+ atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
+ containerAtoms = new ArrayDeque<>();
+ nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
+ nalLength = new ParsableByteArray(4);
+ scratch = new ParsableByteArray();
+ sampleTrackIndex = C.INDEX_UNSET;
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return Sniffer.sniffUnfragmented(input);
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ containerAtoms.clear();
+ atomHeaderBytesRead = 0;
+ sampleTrackIndex = C.INDEX_UNSET;
+ sampleBytesRead = 0;
+ sampleBytesWritten = 0;
+ sampleCurrentNalBytesRemaining = 0;
+ if (position == 0) {
+ enterReadingAtomHeaderState();
+ } else if (tracks != null) {
+ updateSampleIndices(timeUs);
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ while (true) {
+ switch (parserState) {
+ case STATE_READING_ATOM_HEADER:
+ if (!readAtomHeader(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_ATOM_PAYLOAD:
+ if (readAtomPayload(input, seekPosition)) {
+ return RESULT_SEEK;
+ }
+ break;
+ case STATE_READING_SAMPLE:
+ return readSample(input, seekPosition);
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ // SeekMap implementation.
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ if (tracks.length == 0) {
+ return new SeekPoints(SeekPoint.START);
+ }
+
+ long firstTimeUs;
+ long firstOffset;
+ long secondTimeUs = C.TIME_UNSET;
+ long secondOffset = C.POSITION_UNSET;
+
+ // If we have a video track, use it to establish one or two seek points.
+ if (firstVideoTrackIndex != C.INDEX_UNSET) {
+ TrackSampleTable sampleTable = tracks[firstVideoTrackIndex].sampleTable;
+ int sampleIndex = getSynchronizationSampleIndex(sampleTable, timeUs);
+ if (sampleIndex == C.INDEX_UNSET) {
+ return new SeekPoints(SeekPoint.START);
+ }
+ long sampleTimeUs = sampleTable.timestampsUs[sampleIndex];
+ firstTimeUs = sampleTimeUs;
+ firstOffset = sampleTable.offsets[sampleIndex];
+ if (sampleTimeUs < timeUs && sampleIndex < sampleTable.sampleCount - 1) {
+ int secondSampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
+ if (secondSampleIndex != C.INDEX_UNSET && secondSampleIndex != sampleIndex) {
+ secondTimeUs = sampleTable.timestampsUs[secondSampleIndex];
+ secondOffset = sampleTable.offsets[secondSampleIndex];
+ }
+ }
+ } else {
+ firstTimeUs = timeUs;
+ firstOffset = Long.MAX_VALUE;
+ }
+
+ // Take into account other tracks.
+ for (int i = 0; i < tracks.length; i++) {
+ if (i != firstVideoTrackIndex) {
+ TrackSampleTable sampleTable = tracks[i].sampleTable;
+ firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset);
+ if (secondTimeUs != C.TIME_UNSET) {
+ secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset);
+ }
+ }
+ }
+
+ SeekPoint firstSeekPoint = new SeekPoint(firstTimeUs, firstOffset);
+ if (secondTimeUs == C.TIME_UNSET) {
+ return new SeekPoints(firstSeekPoint);
+ } else {
+ SeekPoint secondSeekPoint = new SeekPoint(secondTimeUs, secondOffset);
+ return new SeekPoints(firstSeekPoint, secondSeekPoint);
+ }
+ }
+
+ // Private methods.
+
+ private void enterReadingAtomHeaderState() {
+ parserState = STATE_READING_ATOM_HEADER;
+ atomHeaderBytesRead = 0;
+ }
+
+ private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
+ 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).");
+ }
+
+ if (shouldParseContainerAtom(atomType)) {
+ long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead;
+ if (atomSize != atomHeaderBytesRead && atomType == Atom.TYPE_meta) {
+ maybeSkipRemainingMetaAtomHeaderBytes(input);
+ }
+ containerAtoms.push(new ContainerAtom(atomType, endPosition));
+ if (atomSize == atomHeaderBytesRead) {
+ processAtomEnded(endPosition);
+ } else {
+ // Start reading the first child atom.
+ enterReadingAtomHeaderState();
+ }
+ } else if (shouldParseLeafAtom(atomType)) {
+ // We don't support parsing of leaf atoms that define extended atom sizes, or that have
+ // lengths greater than Integer.MAX_VALUE.
+ Assertions.checkState(atomHeaderBytesRead == Atom.HEADER_SIZE);
+ Assertions.checkState(atomSize <= Integer.MAX_VALUE);
+ atomData = new ParsableByteArray((int) atomSize);
+ System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
+ parserState = STATE_READING_ATOM_PAYLOAD;
+ } else {
+ atomData = null;
+ parserState = STATE_READING_ATOM_PAYLOAD;
+ }
+
+ return true;
+ }
+
+ /**
+ * Processes the atom payload. If {@link #atomData} is null and the size is at or above the
+ * threshold {@link #RELOAD_MINIMUM_SEEK_DISTANCE}, {@code true} is returned and the caller should
+ * restart loading at the position in {@code positionHolder}. Otherwise, the atom is read/skipped.
+ */
+ private boolean readAtomPayload(ExtractorInput input, PositionHolder positionHolder)
+ throws IOException, InterruptedException {
+ long atomPayloadSize = atomSize - atomHeaderBytesRead;
+ long atomEndPosition = input.getPosition() + atomPayloadSize;
+ boolean seekRequired = false;
+ if (atomData != null) {
+ input.readFully(atomData.data, atomHeaderBytesRead, (int) atomPayloadSize);
+ if (atomType == Atom.TYPE_ftyp) {
+ isQuickTime = processFtypAtom(atomData);
+ } else if (!containerAtoms.isEmpty()) {
+ containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData));
+ }
+ } else {
+ // We don't need the data. Skip or seek, depending on how large the atom is.
+ if (atomPayloadSize < RELOAD_MINIMUM_SEEK_DISTANCE) {
+ input.skipFully((int) atomPayloadSize);
+ } else {
+ positionHolder.position = input.getPosition() + atomPayloadSize;
+ seekRequired = true;
+ }
+ }
+ processAtomEnded(atomEndPosition);
+ return seekRequired && parserState != STATE_READING_SAMPLE;
+ }
+
+ private void processAtomEnded(long atomEndPosition) throws ParserException {
+ while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) {
+ Atom.ContainerAtom containerAtom = containerAtoms.pop();
+ if (containerAtom.type == Atom.TYPE_moov) {
+ // We've reached the end of the moov atom. Process it and prepare to read samples.
+ processMoovAtom(containerAtom);
+ containerAtoms.clear();
+ parserState = STATE_READING_SAMPLE;
+ } else if (!containerAtoms.isEmpty()) {
+ containerAtoms.peek().add(containerAtom);
+ }
+ }
+ if (parserState != STATE_READING_SAMPLE) {
+ enterReadingAtomHeaderState();
+ }
+ }
+
+ /**
+ * Updates the stored track metadata to reflect the contents of the specified moov atom.
+ */
+ private void processMoovAtom(ContainerAtom moov) throws ParserException {
+ int firstVideoTrackIndex = C.INDEX_UNSET;
+ long durationUs = C.TIME_UNSET;
+ List<Mp4Track> tracks = new ArrayList<>();
+
+ // Process metadata.
+ Metadata udtaMetadata = null;
+ GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
+ Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
+ if (udta != null) {
+ udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime);
+ if (udtaMetadata != null) {
+ gaplessInfoHolder.setFromMetadata(udtaMetadata);
+ }
+ }
+ Metadata mdtaMetadata = null;
+ Atom.ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta);
+ if (meta != null) {
+ mdtaMetadata = AtomParsers.parseMdtaFromMeta(meta);
+ }
+
+ boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
+ ArrayList<TrackSampleTable> trackSampleTables =
+ getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists);
+
+ int trackCount = trackSampleTables.size();
+ for (int i = 0; i < trackCount; i++) {
+ TrackSampleTable trackSampleTable = trackSampleTables.get(i);
+ Track track = trackSampleTable.track;
+ long trackDurationUs =
+ track.durationUs != C.TIME_UNSET ? track.durationUs : trackSampleTable.durationUs;
+ durationUs = Math.max(durationUs, trackDurationUs);
+ 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_VIDEO
+ && trackDurationUs > 0
+ && trackSampleTable.sampleCount > 1) {
+ float frameRate = trackSampleTable.sampleCount / (trackDurationUs / 1000000f);
+ format = format.copyWithFrameRate(frameRate);
+ }
+ format =
+ MetadataUtil.getFormatWithMetadata(
+ track.type, format, udtaMetadata, mdtaMetadata, gaplessInfoHolder);
+ mp4Track.trackOutput.format(format);
+
+ if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) {
+ firstVideoTrackIndex = tracks.size();
+ }
+ tracks.add(mp4Track);
+ }
+ this.firstVideoTrackIndex = firstVideoTrackIndex;
+ this.durationUs = durationUs;
+ this.tracks = tracks.toArray(new Mp4Track[0]);
+ accumulatedSampleSizes = calculateAccumulatedSampleSizes(this.tracks);
+
+ extractorOutput.endTracks();
+ extractorOutput.seekMap(this);
+ }
+
+ private ArrayList<TrackSampleTable> getTrackSampleTables(
+ ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists)
+ throws ParserException {
+ ArrayList<TrackSampleTable> trackSampleTables = new ArrayList<>();
+ 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),
+ /* duration= */ C.TIME_UNSET,
+ /* drmInitData= */ null,
+ ignoreEditLists,
+ 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;
+ }
+ trackSampleTables.add(trackSampleTable);
+ }
+ return trackSampleTables;
+ }
+
+ /**
+ * Attempts to extract the next sample in the current mdat atom for the specified track.
+ * <p>
+ * Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in
+ * {@code positionHolder}.
+ * <p>
+ * Returns {@link #RESULT_END_OF_INPUT} if no samples are left. Otherwise, returns
+ * {@link #RESULT_CONTINUE}.
+ *
+ * @param input The {@link ExtractorInput} from which to read data.
+ * @param positionHolder If {@link #RESULT_SEEK} is returned, this holder is updated to hold the
+ * position of the required data.
+ * @return One of the {@code RESULT_*} flags in {@link Extractor}.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private int readSample(ExtractorInput input, PositionHolder positionHolder)
+ throws IOException, InterruptedException {
+ long inputPosition = input.getPosition();
+ if (sampleTrackIndex == C.INDEX_UNSET) {
+ sampleTrackIndex = getTrackIndexOfNextReadSample(inputPosition);
+ if (sampleTrackIndex == C.INDEX_UNSET) {
+ return RESULT_END_OF_INPUT;
+ }
+ }
+ Mp4Track track = tracks[sampleTrackIndex];
+ TrackOutput trackOutput = track.trackOutput;
+ int sampleIndex = track.sampleIndex;
+ long position = track.sampleTable.offsets[sampleIndex];
+ int sampleSize = track.sampleTable.sizes[sampleIndex];
+ long skipAmount = position - inputPosition + sampleBytesRead;
+ if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
+ positionHolder.position = position;
+ return RESULT_SEEK;
+ }
+ if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) {
+ // The sample information is contained in a cdat atom. The header must be discarded for
+ // committing.
+ skipAmount += Atom.HEADER_SIZE;
+ sampleSize -= Atom.HEADER_SIZE;
+ }
+ input.skipFully((int) skipAmount);
+ if (track.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[] nalLengthData = nalLength.data;
+ nalLengthData[0] = 0;
+ nalLengthData[1] = 0;
+ nalLengthData[2] = 0;
+ int nalUnitLengthFieldLength = track.track.nalUnitLengthFieldLength;
+ int nalUnitLengthFieldLengthDiff = 4 - track.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.
+ input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
+ sampleBytesRead += nalUnitLengthFieldLength;
+ nalLength.setPosition(0);
+ int nalLengthInt = nalLength.readInt();
+ if (nalLengthInt < 0) {
+ throw new ParserException("Invalid NAL length");
+ }
+ sampleCurrentNalBytesRemaining = nalLengthInt;
+ // Write a start code for the current NAL unit.
+ nalStartCode.setPosition(0);
+ trackOutput.sampleData(nalStartCode, 4);
+ sampleBytesWritten += 4;
+ sampleSize += nalUnitLengthFieldLengthDiff;
+ } else {
+ // Write the payload of the NAL unit.
+ int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false);
+ sampleBytesRead += writtenBytes;
+ sampleBytesWritten += writtenBytes;
+ sampleCurrentNalBytesRemaining -= writtenBytes;
+ }
+ }
+ } else {
+ if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) {
+ if (sampleBytesWritten == 0) {
+ Ac4Util.getAc4SampleHeader(sampleSize, scratch);
+ trackOutput.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE);
+ sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE;
+ }
+ sampleSize += Ac4Util.SAMPLE_HEADER_SIZE;
+ }
+ while (sampleBytesWritten < sampleSize) {
+ int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false);
+ sampleBytesRead += writtenBytes;
+ sampleBytesWritten += writtenBytes;
+ sampleCurrentNalBytesRemaining -= writtenBytes;
+ }
+ }
+ trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex],
+ track.sampleTable.flags[sampleIndex], sampleSize, 0, null);
+ track.sampleIndex++;
+ sampleTrackIndex = C.INDEX_UNSET;
+ sampleBytesRead = 0;
+ sampleBytesWritten = 0;
+ sampleCurrentNalBytesRemaining = 0;
+ return RESULT_CONTINUE;
+ }
+
+ /**
+ * Returns the index of the track that contains the next sample to be read, or {@link
+ * C#INDEX_UNSET} if no samples remain.
+ *
+ * <p>The preferred choice is the sample with the smallest offset not requiring a source reload,
+ * or if not available the sample with the smallest overall offset to avoid subsequent source
+ * reloads.
+ *
+ * <p>To deal with poor sample interleaving, we also check whether the required memory to catch up
+ * with the next logical sample (based on sample time) exceeds {@link
+ * #MAXIMUM_READ_AHEAD_BYTES_STREAM}. If this is the case, we continue with this sample even
+ * though it may require a source reload.
+ */
+ private int getTrackIndexOfNextReadSample(long inputPosition) {
+ long preferredSkipAmount = Long.MAX_VALUE;
+ boolean preferredRequiresReload = true;
+ int preferredTrackIndex = C.INDEX_UNSET;
+ long preferredAccumulatedBytes = Long.MAX_VALUE;
+ long minAccumulatedBytes = Long.MAX_VALUE;
+ boolean minAccumulatedBytesRequiresReload = true;
+ int minAccumulatedBytesTrackIndex = C.INDEX_UNSET;
+ for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
+ Mp4Track track = tracks[trackIndex];
+ int sampleIndex = track.sampleIndex;
+ if (sampleIndex == track.sampleTable.sampleCount) {
+ continue;
+ }
+ long sampleOffset = track.sampleTable.offsets[sampleIndex];
+ long sampleAccumulatedBytes = accumulatedSampleSizes[trackIndex][sampleIndex];
+ long skipAmount = sampleOffset - inputPosition;
+ boolean requiresReload = skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE;
+ if ((!requiresReload && preferredRequiresReload)
+ || (requiresReload == preferredRequiresReload && skipAmount < preferredSkipAmount)) {
+ preferredRequiresReload = requiresReload;
+ preferredSkipAmount = skipAmount;
+ preferredTrackIndex = trackIndex;
+ preferredAccumulatedBytes = sampleAccumulatedBytes;
+ }
+ if (sampleAccumulatedBytes < minAccumulatedBytes) {
+ minAccumulatedBytes = sampleAccumulatedBytes;
+ minAccumulatedBytesRequiresReload = requiresReload;
+ minAccumulatedBytesTrackIndex = trackIndex;
+ }
+ }
+ return minAccumulatedBytes == Long.MAX_VALUE
+ || !minAccumulatedBytesRequiresReload
+ || preferredAccumulatedBytes < minAccumulatedBytes + MAXIMUM_READ_AHEAD_BYTES_STREAM
+ ? preferredTrackIndex
+ : minAccumulatedBytesTrackIndex;
+ }
+
+ /**
+ * Updates every track's sample index to point its latest sync sample before/at {@code timeUs}.
+ */
+ private void updateSampleIndices(long timeUs) {
+ for (Mp4Track track : tracks) {
+ TrackSampleTable sampleTable = track.sampleTable;
+ int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);
+ if (sampleIndex == C.INDEX_UNSET) {
+ // Handle the case where the requested time is before the first synchronization sample.
+ sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
+ }
+ track.sampleIndex = sampleIndex;
+ }
+ }
+
+ /**
+ * Possibly skips the version and flags fields (1+3 byte) of a full meta atom of the {@code
+ * input}.
+ *
+ * <p>Atoms of type {@link Atom#TYPE_meta} are defined to be full atoms which have four additional
+ * bytes for a version and a flags field (see 4.2 'Object Structure' in ISO/IEC 14496-12:2005).
+ * QuickTime do not have such a full box structure. Since some of these files are encoded wrongly,
+ * we can't rely on the file type though. Instead we must check the 8 bytes after the common
+ * header bytes ourselves.
+ */
+ private void maybeSkipRemainingMetaAtomHeaderBytes(ExtractorInput input)
+ throws IOException, InterruptedException {
+ scratch.reset(8);
+ // Peek the next 8 bytes which can be either
+ // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom]
+ // (qt) [4 byte size of next atom ][4 byte hdlr atom type ]
+ // In case of (iso) we need to skip the next 4 bytes.
+ input.peekFully(scratch.data, 0, 8);
+ scratch.skipBytes(4);
+ if (scratch.readInt() == Atom.TYPE_hdlr) {
+ input.resetPeekPosition();
+ } else {
+ input.skipFully(4);
+ }
+ }
+
+ /**
+ * For each sample of each track, calculates accumulated size of all samples which need to be read
+ * before this sample can be used.
+ */
+ private static long[][] calculateAccumulatedSampleSizes(Mp4Track[] tracks) {
+ long[][] accumulatedSampleSizes = new long[tracks.length][];
+ int[] nextSampleIndex = new int[tracks.length];
+ long[] nextSampleTimesUs = new long[tracks.length];
+ boolean[] tracksFinished = new boolean[tracks.length];
+ for (int i = 0; i < tracks.length; i++) {
+ accumulatedSampleSizes[i] = new long[tracks[i].sampleTable.sampleCount];
+ nextSampleTimesUs[i] = tracks[i].sampleTable.timestampsUs[0];
+ }
+ long accumulatedSampleSize = 0;
+ int finishedTracks = 0;
+ while (finishedTracks < tracks.length) {
+ long minTimeUs = Long.MAX_VALUE;
+ int minTimeTrackIndex = -1;
+ for (int i = 0; i < tracks.length; i++) {
+ if (!tracksFinished[i] && nextSampleTimesUs[i] <= minTimeUs) {
+ minTimeTrackIndex = i;
+ minTimeUs = nextSampleTimesUs[i];
+ }
+ }
+ int trackSampleIndex = nextSampleIndex[minTimeTrackIndex];
+ accumulatedSampleSizes[minTimeTrackIndex][trackSampleIndex] = accumulatedSampleSize;
+ accumulatedSampleSize += tracks[minTimeTrackIndex].sampleTable.sizes[trackSampleIndex];
+ nextSampleIndex[minTimeTrackIndex] = ++trackSampleIndex;
+ if (trackSampleIndex < accumulatedSampleSizes[minTimeTrackIndex].length) {
+ nextSampleTimesUs[minTimeTrackIndex] =
+ tracks[minTimeTrackIndex].sampleTable.timestampsUs[trackSampleIndex];
+ } else {
+ tracksFinished[minTimeTrackIndex] = true;
+ finishedTracks++;
+ }
+ }
+ return accumulatedSampleSizes;
+ }
+
+ /**
+ * Adjusts a seek point offset to take into account the track with the given {@code sampleTable},
+ * for a given {@code seekTimeUs}.
+ *
+ * @param sampleTable The sample table to use.
+ * @param seekTimeUs The seek time in microseconds.
+ * @param offset The current offset.
+ * @return The adjusted offset.
+ */
+ private static long maybeAdjustSeekOffset(
+ TrackSampleTable sampleTable, long seekTimeUs, long offset) {
+ int sampleIndex = getSynchronizationSampleIndex(sampleTable, seekTimeUs);
+ if (sampleIndex == C.INDEX_UNSET) {
+ return offset;
+ }
+ long sampleOffset = sampleTable.offsets[sampleIndex];
+ return Math.min(sampleOffset, offset);
+ }
+
+ /**
+ * Returns the index of the synchronization sample before or at {@code timeUs}, or the index of
+ * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} if
+ * there are no synchronization samples in the table.
+ *
+ * @param sampleTable The sample table in which to locate a synchronization sample.
+ * @param timeUs A time in microseconds.
+ * @return The index of the synchronization sample before or at {@code timeUs}, or the index of
+ * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET}
+ * if there are no synchronization samples in the table.
+ */
+ private static int getSynchronizationSampleIndex(TrackSampleTable sampleTable, long timeUs) {
+ int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);
+ if (sampleIndex == C.INDEX_UNSET) {
+ // Handle the case where the requested time is before the first synchronization sample.
+ sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
+ }
+ return sampleIndex;
+ }
+
+ /**
+ * Process an ftyp atom to determine whether the media is QuickTime.
+ *
+ * @param atomData The ftyp atom data.
+ * @return Whether the media is QuickTime.
+ */
+ private static boolean processFtypAtom(ParsableByteArray atomData) {
+ atomData.setPosition(Atom.HEADER_SIZE);
+ int majorBrand = atomData.readInt();
+ if (majorBrand == BRAND_QUICKTIME) {
+ return true;
+ }
+ atomData.skipBytes(4); // minor_version
+ while (atomData.bytesLeft() > 0) {
+ if (atomData.readInt() == BRAND_QUICKTIME) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */
+ private static boolean shouldParseLeafAtom(int atom) {
+ 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_stz2
+ || atom == Atom.TYPE_stco
+ || atom == Atom.TYPE_co64
+ || atom == Atom.TYPE_tkhd
+ || atom == Atom.TYPE_ftyp
+ || atom == Atom.TYPE_udta
+ || atom == Atom.TYPE_keys
+ || atom == Atom.TYPE_ilst;
+ }
+
+ /** 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_edts
+ || atom == Atom.TYPE_meta;
+ }
+
+ private static final class Mp4Track {
+
+ public final Track track;
+ public final TrackSampleTable sampleTable;
+ public final TrackOutput trackOutput;
+
+ public int sampleIndex;
+
+ public Mp4Track(Track track, TrackSampleTable sampleTable, TrackOutput trackOutput) {
+ this.track = track;
+ this.sampleTable = sampleTable;
+ this.trackOutput = trackOutput;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
new file mode 100644
index 0000000000..ddb13aeb9c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
@@ -0,0 +1,208 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.nio.ByteBuffer;
+import java.util.UUID;
+
+/**
+ * Utility methods for handling PSSH atoms.
+ */
+public final class PsshAtomUtil {
+
+ private static final String TAG = "PsshAtomUtil";
+
+ private PsshAtomUtil() {}
+
+ /**
+ * Builds a version 0 PSSH atom for a given system id, containing the given data.
+ *
+ * @param systemId The system id of the scheme.
+ * @param data The scheme specific data.
+ * @return The PSSH atom.
+ */
+ public static byte[] buildPsshAtom(UUID systemId, @Nullable byte[] data) {
+ return buildPsshAtom(systemId, null, data);
+ }
+
+ /**
+ * Builds a PSSH atom for the given system id, containing the given key ids and data.
+ *
+ * @param systemId The system id of the scheme.
+ * @param keyIds The key ids for a version 1 PSSH atom, or null for a version 0 PSSH atom.
+ * @param data The scheme specific data.
+ * @return The PSSH atom.
+ */
+ // dereference of possibly-null reference keyId
+ @SuppressWarnings({"ParameterNotNullable", "nullness:dereference.of.nullable"})
+ public static byte[] buildPsshAtom(
+ UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) {
+ int dataLength = data != null ? data.length : 0;
+ int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* SystemId */ + 4 /* DataSize */ + dataLength;
+ if (keyIds != null) {
+ psshBoxLength += 4 /* KID_count */ + (keyIds.length * 16) /* KIDs */;
+ }
+ ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength);
+ psshBox.putInt(psshBoxLength);
+ psshBox.putInt(Atom.TYPE_pssh);
+ psshBox.putInt(keyIds != null ? 0x01000000 : 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */);
+ psshBox.putLong(systemId.getMostSignificantBits());
+ psshBox.putLong(systemId.getLeastSignificantBits());
+ if (keyIds != null) {
+ psshBox.putInt(keyIds.length);
+ for (UUID keyId : keyIds) {
+ psshBox.putLong(keyId.getMostSignificantBits());
+ psshBox.putLong(keyId.getLeastSignificantBits());
+ }
+ }
+ if (data != null && data.length != 0) {
+ psshBox.putInt(data.length);
+ psshBox.put(data);
+ } // Else the last 4 bytes are a 0 DataSize.
+ return psshBox.array();
+ }
+
+ /**
+ * Returns whether the data is a valid PSSH atom.
+ *
+ * @param data The data to parse.
+ * @return Whether the data is a valid PSSH atom.
+ */
+ public static boolean isPsshAtom(byte[] data) {
+ return parsePsshAtom(data) != null;
+ }
+
+ /**
+ * Parses the UUID from a PSSH atom. Version 0 and 1 PSSH atoms are supported.
+ *
+ * <p>The UUID is only parsed if the data is a valid PSSH atom.
+ *
+ * @param atom The atom to parse.
+ * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has an
+ * unsupported version.
+ */
+ public static @Nullable UUID parseUuid(byte[] atom) {
+ PsshAtom parsedAtom = parsePsshAtom(atom);
+ if (parsedAtom == null) {
+ return null;
+ }
+ return parsedAtom.uuid;
+ }
+
+ /**
+ * Parses the version from a PSSH atom. Version 0 and 1 PSSH atoms are supported.
+ * <p>
+ * The version is only parsed if the data is a valid PSSH atom.
+ *
+ * @param atom The atom to parse.
+ * @return The parsed version. -1 if the input is not a valid PSSH atom, or if the PSSH atom has
+ * an unsupported version.
+ */
+ public static int parseVersion(byte[] atom) {
+ PsshAtom parsedAtom = parsePsshAtom(atom);
+ if (parsedAtom == null) {
+ return -1;
+ }
+ return parsedAtom.version;
+ }
+
+ /**
+ * Parses the scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are supported.
+ *
+ * <p>The scheme specific data is only parsed if the data is a valid PSSH atom matching the given
+ * UUID, or if the data is a valid PSSH atom of any type in the case that the passed UUID is null.
+ *
+ * @param atom The atom to parse.
+ * @param uuid The required UUID of the PSSH atom, or null to accept any UUID.
+ * @return The parsed scheme specific data. Null if the input is not a valid PSSH atom, or if the
+ * PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID.
+ */
+ public static @Nullable byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) {
+ PsshAtom parsedAtom = parsePsshAtom(atom);
+ if (parsedAtom == null) {
+ return null;
+ }
+ if (uuid != null && !uuid.equals(parsedAtom.uuid)) {
+ Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.uuid + ".");
+ return null;
+ }
+ return parsedAtom.schemeData;
+ }
+
+ /**
+ * Parses a PSSH atom. Version 0 and 1 PSSH atoms are supported.
+ *
+ * @param atom The atom to parse.
+ * @return The parsed PSSH atom. Null if the input is not a valid PSSH atom, or if the PSSH atom
+ * has an unsupported version.
+ */
+ // TODO: Support parsing of the key ids for version 1 PSSH atoms.
+ private static @Nullable PsshAtom parsePsshAtom(byte[] atom) {
+ ParsableByteArray atomData = new ParsableByteArray(atom);
+ if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) {
+ // Data too short.
+ return null;
+ }
+ atomData.setPosition(0);
+ int atomSize = atomData.readInt();
+ if (atomSize != atomData.bytesLeft() + 4) {
+ // Not an atom, or incorrect atom size.
+ return null;
+ }
+ int atomType = atomData.readInt();
+ if (atomType != Atom.TYPE_pssh) {
+ // Not an atom, or incorrect atom type.
+ return null;
+ }
+ int atomVersion = Atom.parseFullAtomVersion(atomData.readInt());
+ if (atomVersion > 1) {
+ Log.w(TAG, "Unsupported pssh version: " + atomVersion);
+ return null;
+ }
+ UUID uuid = new UUID(atomData.readLong(), atomData.readLong());
+ if (atomVersion == 1) {
+ int keyIdCount = atomData.readUnsignedIntToInt();
+ atomData.skipBytes(16 * keyIdCount);
+ }
+ int dataSize = atomData.readUnsignedIntToInt();
+ if (dataSize != atomData.bytesLeft()) {
+ // Incorrect dataSize.
+ return null;
+ }
+ byte[] data = new byte[dataSize];
+ atomData.readBytes(data, 0, dataSize);
+ return new PsshAtom(uuid, atomVersion, data);
+ }
+
+ // TODO: Consider exposing this and making parsePsshAtom public.
+ private static class PsshAtom {
+
+ private final UUID uuid;
+ private final int version;
+ private final byte[] schemeData;
+
+ public PsshAtom(UUID uuid, int version, byte[] schemeData) {
+ this.uuid = uuid;
+ this.version = version;
+ this.schemeData = schemeData;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
new file mode 100644
index 0000000000..d58c2f06eb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
@@ -0,0 +1,201 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * Provides methods that peek data from an {@link ExtractorInput} and return whether the input
+ * appears to be in MP4 format.
+ */
+/* package */ final class Sniffer {
+
+ /** The maximum number of bytes to peek when sniffing. */
+ private static final int SEARCH_LENGTH = 4 * 1024;
+
+ private static final int[] COMPATIBLE_BRANDS =
+ new int[] {
+ 0x69736f6d, // isom
+ 0x69736f32, // iso2
+ 0x69736f33, // iso3
+ 0x69736f34, // iso4
+ 0x69736f35, // iso5
+ 0x69736f36, // iso6
+ 0x61766331, // avc1
+ 0x68766331, // hvc1
+ 0x68657631, // hev1
+ 0x61763031, // av01
+ 0x6d703431, // mp41
+ 0x6d703432, // mp42
+ 0x33673261, // 3g2a
+ 0x33673262, // 3g2b
+ 0x33677236, // 3gr6
+ 0x33677336, // 3gs6
+ 0x33676536, // 3ge6
+ 0x33676736, // 3gg6
+ 0x4d345620, // M4V[space]
+ 0x4d344120, // M4A[space]
+ 0x66347620, // f4v[space]
+ 0x6b646469, // kddi
+ 0x4d345650, // M4VP
+ 0x71742020, // qt[space][space], Apple QuickTime
+ 0x4d534e56, // MSNV, Sony PSP
+ 0x64627931, // dby1, Dolby Vision
+ };
+
+ /**
+ * Returns whether data peeked from the current position in {@code input} is consistent with the
+ * input being a fragmented MP4 file.
+ *
+ * @param input The extractor input from which to peek data. The peek position will be modified.
+ * @return Whether the input appears to be in the fragmented MP4 format.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ public static boolean sniffFragmented(ExtractorInput input)
+ throws IOException, InterruptedException {
+ return sniffInternal(input, true);
+ }
+
+ /**
+ * Returns whether data peeked from the current position in {@code input} is consistent with the
+ * input being an unfragmented MP4 file.
+ *
+ * @param input The extractor input from which to peek data. The peek position will be modified.
+ * @return Whether the input appears to be in the unfragmented MP4 format.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ public static boolean sniffUnfragmented(ExtractorInput input)
+ throws IOException, InterruptedException {
+ return sniffInternal(input, false);
+ }
+
+ private static boolean sniffInternal(ExtractorInput input, boolean fragmented)
+ throws IOException, InterruptedException {
+ long inputLength = input.getLength();
+ int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH
+ ? SEARCH_LENGTH : inputLength);
+
+ ParsableByteArray buffer = new ParsableByteArray(64);
+ int bytesSearched = 0;
+ boolean foundGoodFileType = false;
+ boolean isFragmented = false;
+ while (bytesSearched < bytesToSearch) {
+ // Read an atom header.
+ int headerSize = Atom.HEADER_SIZE;
+ buffer.reset(headerSize);
+ input.peekFully(buffer.data, 0, headerSize);
+ long atomSize = buffer.readUnsignedInt();
+ int atomType = buffer.readInt();
+ if (atomSize == Atom.DEFINES_LARGE_SIZE) {
+ // Read the large atom size.
+ headerSize = Atom.LONG_HEADER_SIZE;
+ input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
+ buffer.setLimit(Atom.LONG_HEADER_SIZE);
+ atomSize = buffer.readLong();
+ } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {
+ // The atom extends to the end of the file.
+ long fileEndPosition = input.getLength();
+ if (fileEndPosition != C.LENGTH_UNSET) {
+ atomSize = fileEndPosition - input.getPeekPosition() + headerSize;
+ }
+ }
+
+ if (atomSize < headerSize) {
+ // The file is invalid because the atom size is too small for its header.
+ return false;
+ }
+ bytesSearched += headerSize;
+
+ if (atomType == Atom.TYPE_moov) {
+ // We have seen the moov atom. We increase the search size to make sure we don't miss an
+ // mvex atom because the moov's size exceeds the search length.
+ bytesToSearch += (int) atomSize;
+ if (inputLength != C.LENGTH_UNSET && bytesToSearch > inputLength) {
+ // Make sure we don't exceed the file size.
+ bytesToSearch = (int) inputLength;
+ }
+ // Check for an mvex atom inside the moov atom to identify whether the file is fragmented.
+ continue;
+ }
+
+ if (atomType == Atom.TYPE_moof || atomType == Atom.TYPE_mvex) {
+ // The movie is fragmented. Stop searching as we must have read any ftyp atom already.
+ isFragmented = true;
+ break;
+ }
+
+ if (bytesSearched + atomSize - headerSize >= bytesToSearch) {
+ // Stop searching as peeking this atom would exceed the search limit.
+ break;
+ }
+
+ int atomDataSize = (int) (atomSize - headerSize);
+ bytesSearched += atomDataSize;
+ if (atomType == Atom.TYPE_ftyp) {
+ // Parse the atom and check the file type/brand is compatible with the extractors.
+ if (atomDataSize < 8) {
+ return false;
+ }
+ buffer.reset(atomDataSize);
+ input.peekFully(buffer.data, 0, atomDataSize);
+ int brandsCount = atomDataSize / 4;
+ for (int i = 0; i < brandsCount; i++) {
+ if (i == 1) {
+ // This index refers to the minorVersion, not a brand, so skip it.
+ buffer.skipBytes(4);
+ } else if (isCompatibleBrand(buffer.readInt())) {
+ foundGoodFileType = true;
+ break;
+ }
+ }
+ if (!foundGoodFileType) {
+ // The types were not compatible and there is only one ftyp atom, so reject the file.
+ return false;
+ }
+ } else if (atomDataSize != 0) {
+ // Skip the atom.
+ input.advancePeekPosition(atomDataSize);
+ }
+ }
+ return foundGoodFileType && fragmented == isFragmented;
+ }
+
+ /**
+ * Returns whether {@code brand} is an ftyp atom brand that is compatible with the MP4 extractors.
+ */
+ private static boolean isCompatibleBrand(int brand) {
+ // Accept all brands starting '3gp'.
+ if (brand >>> 8 == 0x00336770) {
+ return true;
+ }
+ for (int compatibleBrand : COMPATIBLE_BRANDS) {
+ if (compatibleBrand == brand) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Sniffer() {
+ // Prevent instantiation.
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java
new file mode 100644
index 0000000000..b7a1555a76
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java
@@ -0,0 +1,148 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Encapsulates information describing an MP4 track.
+ */
+public final class Track {
+
+ /**
+ * The transformation to apply to samples in the track, if any. One of {@link
+ * #TRANSFORMATION_NONE} or {@link #TRANSFORMATION_CEA608_CDAT}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TRANSFORMATION_NONE, TRANSFORMATION_CEA608_CDAT})
+ public @interface Transformation {}
+ /**
+ * A no-op sample transformation.
+ */
+ public static final int TRANSFORMATION_NONE = 0;
+ /**
+ * A transformation for caption samples in cdat atoms.
+ */
+ public static final int TRANSFORMATION_CEA608_CDAT = 1;
+
+ /**
+ * The track identifier.
+ */
+ public final int id;
+
+ /**
+ * One of {@link C#TRACK_TYPE_AUDIO}, {@link C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_TEXT}.
+ */
+ public final int type;
+
+ /**
+ * The track timescale, defined as the number of time units that pass in one second.
+ */
+ public final long timescale;
+
+ /**
+ * The movie timescale.
+ */
+ public final long movieTimescale;
+
+ /**
+ * The duration of the track in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public final long durationUs;
+
+ /**
+ * The format.
+ */
+ public final Format format;
+
+ /**
+ * One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each
+ * sample.
+ */
+ @Transformation public final int sampleTransformation;
+
+ /**
+ * Durations of edit list segments in the movie timescale. Null if there is no edit list.
+ */
+ @Nullable public final long[] editListDurations;
+
+ /**
+ * Media times for edit list segments in the track timescale. Null if there is no edit list.
+ */
+ @Nullable public final long[] editListMediaTimes;
+
+ /**
+ * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for
+ * other track types.
+ */
+ public final int nalUnitLengthFieldLength;
+
+ @Nullable private final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes;
+
+ public Track(int id, int type, long timescale, long movieTimescale, long durationUs,
+ Format format, @Transformation int sampleTransformation,
+ @Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength,
+ @Nullable long[] editListDurations, @Nullable long[] editListMediaTimes) {
+ this.id = id;
+ this.type = type;
+ this.timescale = timescale;
+ this.movieTimescale = movieTimescale;
+ this.durationUs = durationUs;
+ this.format = format;
+ this.sampleTransformation = sampleTransformation;
+ this.sampleDescriptionEncryptionBoxes = sampleDescriptionEncryptionBoxes;
+ this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;
+ this.editListDurations = editListDurations;
+ this.editListMediaTimes = editListMediaTimes;
+ }
+
+ /**
+ * Returns the {@link TrackEncryptionBox} for the given sample description index.
+ *
+ * @param sampleDescriptionIndex The given sample description index
+ * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no
+ * such entry exists.
+ */
+ @Nullable
+ public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) {
+ return sampleDescriptionEncryptionBoxes == null ? null
+ : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex];
+ }
+
+ // incompatible types in argument.
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ public Track copyWithFormat(Format format) {
+ return new Track(
+ id,
+ type,
+ timescale,
+ movieTimescale,
+ durationUs,
+ format,
+ sampleTransformation,
+ sampleDescriptionEncryptionBoxes,
+ nalUnitLengthFieldLength,
+ editListDurations,
+ editListMediaTimes);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java
new file mode 100644
index 0000000000..04bfb82210
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java
@@ -0,0 +1,103 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+
+/**
+ * Encapsulates information parsed from a track encryption (tenc) box or sample group description
+ * (sgpd) box in an MP4 stream.
+ */
+public final class TrackEncryptionBox {
+
+ private static final String TAG = "TrackEncryptionBox";
+
+ /**
+ * Indicates the encryption state of the samples in the sample group.
+ */
+ public final boolean isEncrypted;
+
+ /**
+ * The protection scheme type, as defined by the 'schm' box, or null if unknown.
+ */
+ @Nullable public final String schemeType;
+
+ /**
+ * A {@link TrackOutput.CryptoData} instance containing the encryption information from this
+ * {@link TrackEncryptionBox}.
+ */
+ public final TrackOutput.CryptoData cryptoData;
+
+ /** The initialization vector size in bytes for the samples in the corresponding sample group. */
+ public final int perSampleIvSize;
+
+ /**
+ * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the
+ * track encryption box or sample group description box. Null otherwise.
+ */
+ @Nullable public final byte[] defaultInitializationVector;
+
+ /**
+ * @param isEncrypted See {@link #isEncrypted}.
+ * @param schemeType See {@link #schemeType}.
+ * @param perSampleIvSize See {@link #perSampleIvSize}.
+ * @param keyId See {@link TrackOutput.CryptoData#encryptionKey}.
+ * @param defaultEncryptedBlocks See {@link TrackOutput.CryptoData#encryptedBlocks}.
+ * @param defaultClearBlocks See {@link TrackOutput.CryptoData#clearBlocks}.
+ * @param defaultInitializationVector See {@link #defaultInitializationVector}.
+ */
+ public TrackEncryptionBox(
+ boolean isEncrypted,
+ @Nullable String schemeType,
+ int perSampleIvSize,
+ byte[] keyId,
+ int defaultEncryptedBlocks,
+ int defaultClearBlocks,
+ @Nullable byte[] defaultInitializationVector) {
+ Assertions.checkArgument(perSampleIvSize == 0 ^ defaultInitializationVector == null);
+ this.isEncrypted = isEncrypted;
+ this.schemeType = schemeType;
+ this.perSampleIvSize = perSampleIvSize;
+ this.defaultInitializationVector = defaultInitializationVector;
+ cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId,
+ defaultEncryptedBlocks, defaultClearBlocks);
+ }
+
+ @C.CryptoMode
+ private static int schemeToCryptoMode(@Nullable String schemeType) {
+ if (schemeType == null) {
+ // If unknown, assume cenc.
+ return C.CRYPTO_MODE_AES_CTR;
+ }
+ switch (schemeType) {
+ case C.CENC_TYPE_cenc:
+ case C.CENC_TYPE_cens:
+ return C.CRYPTO_MODE_AES_CTR;
+ case C.CENC_TYPE_cbc1:
+ case C.CENC_TYPE_cbcs:
+ return C.CRYPTO_MODE_AES_CBC;
+ default:
+ Log.w(TAG, "Unsupported protection scheme type '" + schemeType + "'. Assuming AES-CTR "
+ + "crypto mode.");
+ return C.CRYPTO_MODE_AES_CTR;
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
new file mode 100644
index 0000000000..e027d6ed76
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
@@ -0,0 +1,197 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * A holder for information corresponding to a single fragment of an mp4 file.
+ */
+/* package */ final class TrackFragment {
+
+ /**
+ * The default values for samples from the track fragment header.
+ */
+ public DefaultSampleValues header;
+ /**
+ * The position (byte offset) of the start of fragment.
+ */
+ public long atomPosition;
+ /**
+ * The position (byte offset) of the start of data contained in the fragment.
+ */
+ public long dataPosition;
+ /**
+ * The position (byte offset) of the start of auxiliary data.
+ */
+ public long auxiliaryDataPosition;
+ /**
+ * The number of track runs of the fragment.
+ */
+ public int trunCount;
+ /**
+ * The total number of samples in the fragment.
+ */
+ public int sampleCount;
+ /**
+ * The position (byte offset) of the start of sample data of each track run in the fragment.
+ */
+ public long[] trunDataPosition;
+ /**
+ * The number of samples contained by each track run in the fragment.
+ */
+ public int[] trunLength;
+ /**
+ * The size of each sample in the fragment.
+ */
+ public int[] sampleSizeTable;
+ /**
+ * The composition time offset of each sample in the fragment.
+ */
+ public int[] sampleCompositionTimeOffsetTable;
+ /**
+ * The decoding time of each sample in the fragment.
+ */
+ public long[] sampleDecodingTimeTable;
+ /**
+ * Indicates which samples are sync frames.
+ */
+ public boolean[] sampleIsSyncFrameTable;
+ /**
+ * Whether the fragment defines encryption data.
+ */
+ public boolean definesEncryptionData;
+ /**
+ * If {@link #definesEncryptionData} is true, indicates which samples use sub-sample encryption.
+ * Undefined otherwise.
+ */
+ public boolean[] sampleHasSubsampleEncryptionTable;
+ /**
+ * Fragment specific track encryption. May be null.
+ */
+ public TrackEncryptionBox trackEncryptionBox;
+ /**
+ * If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data.
+ * Undefined otherwise.
+ */
+ public int sampleEncryptionDataLength;
+ /**
+ * If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined
+ * otherwise.
+ */
+ public ParsableByteArray sampleEncryptionData;
+ /**
+ * Whether {@link #sampleEncryptionData} needs populating with the actual encryption data.
+ */
+ public boolean sampleEncryptionDataNeedsFill;
+ /**
+ * The absolute decode time of the start of the next fragment.
+ */
+ public long nextFragmentDecodeTime;
+
+ /**
+ * Resets the fragment.
+ * <p>
+ * {@link #sampleCount} and {@link #nextFragmentDecodeTime} are set to 0, and both
+ * {@link #definesEncryptionData} and {@link #sampleEncryptionDataNeedsFill} is set to false,
+ * and {@link #trackEncryptionBox} is set to null.
+ */
+ public void reset() {
+ trunCount = 0;
+ nextFragmentDecodeTime = 0;
+ definesEncryptionData = false;
+ sampleEncryptionDataNeedsFill = false;
+ trackEncryptionBox = null;
+ }
+
+ /**
+ * Configures the fragment for the specified number of samples.
+ * <p>
+ * The {@link #sampleCount} of the fragment is set to the specified sample count, and the
+ * contained tables are resized if necessary such that they are at least this length.
+ *
+ * @param sampleCount The number of samples in the new run.
+ */
+ public void initTables(int trunCount, int sampleCount) {
+ this.trunCount = trunCount;
+ this.sampleCount = sampleCount;
+ if (trunLength == null || trunLength.length < trunCount) {
+ trunDataPosition = new long[trunCount];
+ trunLength = new int[trunCount];
+ }
+ if (sampleSizeTable == null || sampleSizeTable.length < sampleCount) {
+ // Size the tables 25% larger than needed, so as to make future resize operations less
+ // likely. The choice of 25% is relatively arbitrary.
+ int tableSize = (sampleCount * 125) / 100;
+ sampleSizeTable = new int[tableSize];
+ sampleCompositionTimeOffsetTable = new int[tableSize];
+ sampleDecodingTimeTable = new long[tableSize];
+ sampleIsSyncFrameTable = new boolean[tableSize];
+ sampleHasSubsampleEncryptionTable = new boolean[tableSize];
+ }
+ }
+
+ /**
+ * Configures the fragment to be one that defines encryption data of the specified length.
+ * <p>
+ * {@link #definesEncryptionData} is set to true, {@link #sampleEncryptionDataLength} is set to
+ * the specified length, and {@link #sampleEncryptionData} is resized if necessary such that it
+ * is at least this length.
+ *
+ * @param length The length in bytes of the encryption data.
+ */
+ public void initEncryptionData(int length) {
+ if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) {
+ sampleEncryptionData = new ParsableByteArray(length);
+ }
+ sampleEncryptionDataLength = length;
+ definesEncryptionData = true;
+ sampleEncryptionDataNeedsFill = true;
+ }
+
+ /**
+ * Fills {@link #sampleEncryptionData} from the provided input.
+ *
+ * @param input An {@link ExtractorInput} from which to read the encryption data.
+ */
+ public void fillEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
+ input.readFully(sampleEncryptionData.data, 0, sampleEncryptionDataLength);
+ sampleEncryptionData.setPosition(0);
+ sampleEncryptionDataNeedsFill = false;
+ }
+
+ /**
+ * Fills {@link #sampleEncryptionData} from the provided source.
+ *
+ * @param source A source from which to read the encryption data.
+ */
+ public void fillEncryptionData(ParsableByteArray source) {
+ source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionDataLength);
+ sampleEncryptionData.setPosition(0);
+ sampleEncryptionDataNeedsFill = false;
+ }
+
+ public long getSamplePresentationTime(int index) {
+ return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index];
+ }
+
+ /** Returns whether the sample at the given index has a subsample encryption table. */
+ public boolean sampleHasSubsampleEncryptionTable(int index) {
+ return definesEncryptionData && sampleHasSubsampleEncryptionTable[index];
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
new file mode 100644
index 0000000000..bb9891b302
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
@@ -0,0 +1,108 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/**
+ * Sample table for a track in an MP4 file.
+ */
+/* package */ final class TrackSampleTable {
+
+ /** The track corresponding to this sample table. */
+ public final Track track;
+ /** Number of samples. */
+ public final int sampleCount;
+ /** Sample offsets in bytes. */
+ public final long[] offsets;
+ /** Sample sizes in bytes. */
+ public final int[] sizes;
+ /** Maximum sample size in {@link #sizes}. */
+ public final int maximumSize;
+ /** Sample timestamps in microseconds. */
+ public final long[] timestampsUs;
+ /** Sample flags. */
+ public final int[] flags;
+ /**
+ * The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample
+ * table is empty.
+ */
+ public final long durationUs;
+
+ public TrackSampleTable(
+ Track track,
+ long[] offsets,
+ int[] sizes,
+ int maximumSize,
+ long[] timestampsUs,
+ int[] flags,
+ long durationUs) {
+ Assertions.checkArgument(sizes.length == timestampsUs.length);
+ Assertions.checkArgument(offsets.length == timestampsUs.length);
+ Assertions.checkArgument(flags.length == timestampsUs.length);
+
+ this.track = track;
+ this.offsets = offsets;
+ this.sizes = sizes;
+ this.maximumSize = maximumSize;
+ this.timestampsUs = timestampsUs;
+ this.flags = flags;
+ this.durationUs = durationUs;
+ sampleCount = offsets.length;
+ if (flags.length > 0) {
+ flags[flags.length - 1] |= C.BUFFER_FLAG_LAST_SAMPLE;
+ }
+ }
+
+ /**
+ * Returns the sample index of the closest synchronization sample at or before the given
+ * timestamp, if one is available.
+ *
+ * @param timeUs Timestamp adjacent to which to find a synchronization sample.
+ * @return Index of the synchronization sample, or {@link C#INDEX_UNSET} if none.
+ */
+ public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) {
+ // Video frame timestamps may not be sorted, so the behavior of this call can be undefined.
+ // Frames are not reordered past synchronization samples so this works in practice.
+ int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false);
+ for (int i = startIndex; i >= 0; i--) {
+ if ((flags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns the sample index of the closest synchronization sample at or after the given timestamp,
+ * if one is available.
+ *
+ * @param timeUs Timestamp adjacent to which to find a synchronization sample.
+ * @return index Index of the synchronization sample, or {@link C#INDEX_UNSET} if none.
+ */
+ public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) {
+ int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false);
+ for (int i = startIndex; i < timestampsUs.length; i++) {
+ if ((flags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java
new file mode 100644
index 0000000000..5d3b27e294
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java
@@ -0,0 +1,313 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg;
+
+import androidx.annotation.VisibleForTesting;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+
+/** Seeks in an Ogg stream. */
+/* package */ final class DefaultOggSeeker implements OggSeeker {
+
+ private static final int MATCH_RANGE = 72000;
+ private static final int MATCH_BYTE_RANGE = 100000;
+ private static final int DEFAULT_OFFSET = 30000;
+
+ private static final int STATE_SEEK_TO_END = 0;
+ private static final int STATE_READ_LAST_PAGE = 1;
+ private static final int STATE_SEEK = 2;
+ private static final int STATE_SKIP = 3;
+ private static final int STATE_IDLE = 4;
+
+ private final OggPageHeader pageHeader = new OggPageHeader();
+ private final long payloadStartPosition;
+ private final long payloadEndPosition;
+ private final StreamReader streamReader;
+
+ private int state;
+ private long totalGranules;
+ private long positionBeforeSeekToEnd;
+ private long targetGranule;
+
+ private long start;
+ private long end;
+ private long startGranule;
+ private long endGranule;
+
+ /**
+ * Constructs an OggSeeker.
+ *
+ * @param streamReader The {@link StreamReader} that owns this seeker.
+ * @param payloadStartPosition Start position of the payload (inclusive).
+ * @param payloadEndPosition End position of the payload (exclusive).
+ * @param firstPayloadPageSize The total size of the first payload page, in bytes.
+ * @param firstPayloadPageGranulePosition The granule position of the first payload page.
+ * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page.
+ */
+ public DefaultOggSeeker(
+ StreamReader streamReader,
+ long payloadStartPosition,
+ long payloadEndPosition,
+ long firstPayloadPageSize,
+ long firstPayloadPageGranulePosition,
+ boolean firstPayloadPageIsLastPage) {
+ Assertions.checkArgument(
+ payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition);
+ this.streamReader = streamReader;
+ this.payloadStartPosition = payloadStartPosition;
+ this.payloadEndPosition = payloadEndPosition;
+ if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition
+ || firstPayloadPageIsLastPage) {
+ totalGranules = firstPayloadPageGranulePosition;
+ state = STATE_IDLE;
+ } else {
+ state = STATE_SEEK_TO_END;
+ }
+ }
+
+ @Override
+ @SuppressWarnings("fallthrough")
+ public long read(ExtractorInput input) throws IOException, InterruptedException {
+ switch (state) {
+ case STATE_IDLE:
+ return -1;
+ case STATE_SEEK_TO_END:
+ positionBeforeSeekToEnd = input.getPosition();
+ state = STATE_READ_LAST_PAGE;
+ // Seek to the end just before the last page of stream to get the duration.
+ long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE;
+ if (lastPageSearchPosition > positionBeforeSeekToEnd) {
+ return lastPageSearchPosition;
+ }
+ // Fall through.
+ case STATE_READ_LAST_PAGE:
+ totalGranules = readGranuleOfLastPage(input);
+ state = STATE_IDLE;
+ return positionBeforeSeekToEnd;
+ case STATE_SEEK:
+ long position = getNextSeekPosition(input);
+ if (position != C.POSITION_UNSET) {
+ return position;
+ }
+ state = STATE_SKIP;
+ // Fall through.
+ case STATE_SKIP:
+ skipToPageOfTargetGranule(input);
+ state = STATE_IDLE;
+ return -(startGranule + 2);
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ }
+
+ @Override
+ public OggSeekMap createSeekMap() {
+ return totalGranules != 0 ? new OggSeekMap() : null;
+ }
+
+ @Override
+ public void startSeek(long targetGranule) {
+ this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1);
+ state = STATE_SEEK;
+ start = payloadStartPosition;
+ end = payloadEndPosition;
+ startGranule = 0;
+ endGranule = totalGranules;
+ }
+
+ /**
+ * Performs a single step of a seeking binary search, returning the byte position from which data
+ * should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged.
+ * If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be
+ * called to skip to the target page.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @return The byte position from which data should be provided for the next step, or {@link
+ * C#POSITION_UNSET} if the search has converged.
+ * @throws IOException If reading from the input fails.
+ * @throws InterruptedException If interrupted while reading from the input.
+ */
+ private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException {
+ if (start == end) {
+ return C.POSITION_UNSET;
+ }
+
+ long currentPosition = input.getPosition();
+ if (!skipToNextPage(input, end)) {
+ if (start == currentPosition) {
+ throw new IOException("No ogg page can be found.");
+ }
+ return start;
+ }
+
+ pageHeader.populate(input, /* quiet= */ false);
+ input.resetPeekPosition();
+
+ long granuleDistance = targetGranule - pageHeader.granulePosition;
+ int pageSize = pageHeader.headerSize + pageHeader.bodySize;
+ if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) {
+ return C.POSITION_UNSET;
+ }
+
+ if (granuleDistance < 0) {
+ end = currentPosition;
+ endGranule = pageHeader.granulePosition;
+ } else {
+ start = input.getPosition() + pageSize;
+ startGranule = pageHeader.granulePosition;
+ }
+
+ if (end - start < MATCH_BYTE_RANGE) {
+ end = start;
+ return start;
+ }
+
+ long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L);
+ long nextPosition =
+ input.getPosition()
+ - offset
+ + (granuleDistance * (end - start) / (endGranule - startGranule));
+ return Util.constrainValue(nextPosition, start, end - 1);
+ }
+
+ /**
+ * Skips forward to the start of the page containing the {@code targetGranule}.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @throws ParserException If populating the page header fails.
+ * @throws IOException If reading from the input fails.
+ * @throws InterruptedException If interrupted while reading from the input.
+ */
+ private void skipToPageOfTargetGranule(ExtractorInput input)
+ throws IOException, InterruptedException {
+ pageHeader.populate(input, /* quiet= */ false);
+ while (pageHeader.granulePosition <= targetGranule) {
+ input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
+ start = input.getPosition();
+ startGranule = pageHeader.granulePosition;
+ pageHeader.populate(input, /* quiet= */ false);
+ }
+ input.resetPeekPosition();
+ }
+
+ /**
+ * Skips to the next page.
+ *
+ * @param input The {@code ExtractorInput} to skip to the next page.
+ * @throws IOException If peeking/reading from the input fails.
+ * @throws InterruptedException If the thread is interrupted.
+ * @throws EOFException If the next page can't be found before the end of the input.
+ */
+ @VisibleForTesting
+ void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException {
+ if (!skipToNextPage(input, payloadEndPosition)) {
+ // Not found until eof.
+ throw new EOFException();
+ }
+ }
+
+ /**
+ * Skips to the next page. Searches for the next page header.
+ *
+ * @param input The {@code ExtractorInput} to skip to the next page.
+ * @param limit The limit up to which the search should take place.
+ * @return Whether the next page was found.
+ * @throws IOException If peeking/reading from the input fails.
+ * @throws InterruptedException If interrupted while peeking/reading from the input.
+ */
+ private boolean skipToNextPage(ExtractorInput input, long limit)
+ throws IOException, InterruptedException {
+ limit = Math.min(limit + 3, payloadEndPosition);
+ byte[] buffer = new byte[2048];
+ int peekLength = buffer.length;
+ while (true) {
+ if (input.getPosition() + peekLength > limit) {
+ // Make sure to not peek beyond the end of the input.
+ peekLength = (int) (limit - input.getPosition());
+ if (peekLength < 4) {
+ // Not found until end.
+ return false;
+ }
+ }
+ input.peekFully(buffer, 0, peekLength, false);
+ for (int i = 0; i < peekLength - 3; i++) {
+ if (buffer[i] == 'O'
+ && buffer[i + 1] == 'g'
+ && buffer[i + 2] == 'g'
+ && buffer[i + 3] == 'S') {
+ // Match! Skip to the start of the pattern.
+ input.skipFully(i);
+ return true;
+ }
+ }
+ // Overlap by not skipping the entire peekLength.
+ input.skipFully(peekLength - 3);
+ }
+ }
+
+ /**
+ * Skips to the last Ogg page in the stream and reads the header's granule field which is the
+ * total number of samples per channel.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @return The total number of samples of this input.
+ * @throws IOException If reading from the input fails.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ @VisibleForTesting
+ long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException {
+ skipToNextPage(input);
+ pageHeader.reset();
+ while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) {
+ pageHeader.populate(input, /* quiet= */ false);
+ input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
+ }
+ return pageHeader.granulePosition;
+ }
+
+ private final class OggSeekMap implements SeekMap {
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ long targetGranule = streamReader.convertTimeToGranule(timeUs);
+ long estimatedPosition =
+ payloadStartPosition
+ + (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules)
+ - DEFAULT_OFFSET;
+ estimatedPosition =
+ Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1);
+ return new SeekPoints(new SeekPoint(timeUs, estimatedPosition));
+ }
+
+ @Override
+ public long getDurationUs() {
+ return streamReader.convertGranuleToTime(totalGranules);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java
new file mode 100644
index 0000000000..449bf35f78
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java
@@ -0,0 +1,143 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacMetadataReader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * {@link StreamReader} to extract Flac data out of Ogg byte stream.
+ */
+/* package */ final class FlacReader extends StreamReader {
+
+ private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF;
+
+ private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
+
+ private FlacStreamMetadata streamMetadata;
+ private FlacOggSeeker flacOggSeeker;
+
+ public static boolean verifyBitstreamType(ParsableByteArray data) {
+ return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type
+ data.readUnsignedInt() == 0x464C4143; // ASCII signature "FLAC"
+ }
+
+ @Override
+ protected void reset(boolean headerData) {
+ super.reset(headerData);
+ if (headerData) {
+ streamMetadata = null;
+ flacOggSeeker = null;
+ }
+ }
+
+ private static boolean isAudioPacket(byte[] data) {
+ return data[0] == AUDIO_PACKET_TYPE;
+ }
+
+ @Override
+ protected long preparePayload(ParsableByteArray packet) {
+ if (!isAudioPacket(packet.data)) {
+ return -1;
+ }
+ return getFlacFrameBlockSize(packet);
+ }
+
+ @Override
+ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
+ byte[] data = packet.data;
+ if (streamMetadata == null) {
+ streamMetadata = new FlacStreamMetadata(data, 17);
+ byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
+ setupData.format = streamMetadata.getFormat(metadata, /* id3Metadata= */ null);
+ } else if ((data[0] & 0x7F) == FlacConstants.METADATA_TYPE_SEEK_TABLE) {
+ flacOggSeeker = new FlacOggSeeker();
+ FlacStreamMetadata.SeekTable seekTable =
+ FlacMetadataReader.readSeekTableMetadataBlock(packet);
+ streamMetadata = streamMetadata.copyWithSeekTable(seekTable);
+ } else if (isAudioPacket(data)) {
+ if (flacOggSeeker != null) {
+ flacOggSeeker.setFirstFrameOffset(position);
+ setupData.oggSeeker = flacOggSeeker;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private int getFlacFrameBlockSize(ParsableByteArray packet) {
+ int blockSizeKey = (packet.data[2] & 0xFF) >> 4;
+ if (blockSizeKey == 6 || blockSizeKey == 7) {
+ // Skip the sample number.
+ packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET);
+ packet.readUtf8EncodedLong();
+ }
+ int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(packet, blockSizeKey);
+ packet.setPosition(0);
+ return result;
+ }
+
+ private class FlacOggSeeker implements OggSeeker {
+
+ private long firstFrameOffset;
+ private long pendingSeekGranule;
+
+ public FlacOggSeeker() {
+ firstFrameOffset = -1;
+ pendingSeekGranule = -1;
+ }
+
+ public void setFirstFrameOffset(long firstFrameOffset) {
+ this.firstFrameOffset = firstFrameOffset;
+ }
+
+ @Override
+ public long read(ExtractorInput input) throws IOException, InterruptedException {
+ if (pendingSeekGranule >= 0) {
+ long result = -(pendingSeekGranule + 2);
+ pendingSeekGranule = -1;
+ return result;
+ }
+ return -1;
+ }
+
+ @Override
+ public void startSeek(long targetGranule) {
+ Assertions.checkNotNull(streamMetadata.seekTable);
+ long[] seekPointGranules = streamMetadata.seekTable.pointSampleNumbers;
+ int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true);
+ pendingSeekGranule = seekPointGranules[index];
+ }
+
+ @Override
+ public SeekMap createSeekMap() {
+ Assertions.checkState(firstFrameOffset != -1);
+ return new FlacSeekTableSeekMap(streamMetadata, firstFrameOffset);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
new file mode 100644
index 0000000000..da53a47dc0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
@@ -0,0 +1,114 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * Extracts data from the Ogg container format.
+ */
+public class OggExtractor implements Extractor {
+
+ /** Factory for {@link OggExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new OggExtractor()};
+
+ private static final int MAX_VERIFICATION_BYTES = 8;
+
+ private ExtractorOutput output;
+ private StreamReader streamReader;
+ private boolean streamReaderInitialized;
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ try {
+ return sniffInternal(input);
+ } catch (ParserException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.output = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ if (streamReader != null) {
+ streamReader.seek(position, timeUs);
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ if (streamReader == null) {
+ if (!sniffInternal(input)) {
+ throw new ParserException("Failed to determine bitstream type");
+ }
+ input.resetPeekPosition();
+ }
+ if (!streamReaderInitialized) {
+ TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
+ output.endTracks();
+ streamReader.init(output, trackOutput);
+ streamReaderInitialized = true;
+ }
+ return streamReader.read(input, seekPosition);
+ }
+
+ private boolean sniffInternal(ExtractorInput input) throws IOException, InterruptedException {
+ OggPageHeader header = new OggPageHeader();
+ if (!header.populate(input, true) || (header.type & 0x02) != 0x02) {
+ return false;
+ }
+
+ int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES);
+ ParsableByteArray scratch = new ParsableByteArray(length);
+ input.peekFully(scratch.data, 0, length);
+
+ if (FlacReader.verifyBitstreamType(resetPosition(scratch))) {
+ streamReader = new FlacReader();
+ } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) {
+ streamReader = new VorbisReader();
+ } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) {
+ streamReader = new OpusReader();
+ } else {
+ return false;
+ }
+ return true;
+ }
+
+ private static ParsableByteArray resetPosition(ParsableByteArray scratch) {
+ scratch.setPosition(0);
+ return scratch;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java
new file mode 100644
index 0000000000..1f3bf38c73
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java
@@ -0,0 +1,155 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * OGG packet class.
+ */
+/* package */ final class OggPacket {
+
+ private final OggPageHeader pageHeader = new OggPageHeader();
+ private final ParsableByteArray packetArray = new ParsableByteArray(
+ new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0);
+
+ private int currentSegmentIndex = C.INDEX_UNSET;
+ private int segmentCount;
+ private boolean populated;
+
+ /**
+ * Resets this reader.
+ */
+ public void reset() {
+ pageHeader.reset();
+ packetArray.reset();
+ currentSegmentIndex = C.INDEX_UNSET;
+ populated = false;
+ }
+
+ /**
+ * Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make
+ * sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader
+ * can resume properly from an error while reading a continued packet spanned across multiple
+ * pages.
+ *
+ * @param input The {@link ExtractorInput} to read data from.
+ * @return {@code true} if the read was successful. The read fails if the end of the input is
+ * encountered without reading data.
+ * @throws IOException If reading from the input fails.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public boolean populate(ExtractorInput input) throws IOException, InterruptedException {
+ Assertions.checkState(input != null);
+
+ if (populated) {
+ populated = false;
+ packetArray.reset();
+ }
+
+ while (!populated) {
+ if (currentSegmentIndex < 0) {
+ // We're at the start of a page.
+ if (!pageHeader.populate(input, true)) {
+ return false;
+ }
+ int segmentIndex = 0;
+ int bytesToSkip = pageHeader.headerSize;
+ if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) {
+ // After seeking, the first packet may be the remainder
+ // part of a continued packet which has to be discarded.
+ bytesToSkip += calculatePacketSize(segmentIndex);
+ segmentIndex += segmentCount;
+ }
+ input.skipFully(bytesToSkip);
+ currentSegmentIndex = segmentIndex;
+ }
+
+ int size = calculatePacketSize(currentSegmentIndex);
+ int segmentIndex = currentSegmentIndex + segmentCount;
+ if (size > 0) {
+ if (packetArray.capacity() < packetArray.limit() + size) {
+ packetArray.data = Arrays.copyOf(packetArray.data, packetArray.limit() + size);
+ }
+ input.readFully(packetArray.data, packetArray.limit(), size);
+ packetArray.setLimit(packetArray.limit() + size);
+ populated = pageHeader.laces[segmentIndex - 1] != 255;
+ }
+ // Advance now since we are sure reading didn't throw an exception.
+ currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? C.INDEX_UNSET
+ : segmentIndex;
+ }
+ return true;
+ }
+
+ /**
+ * An OGG Packet may span multiple pages. Returns the {@link OggPageHeader} of the last page read,
+ * or an empty header if the packet has yet to be populated.
+ *
+ * <p>Note that the returned {@link OggPageHeader} is mutable and may be updated during subsequent
+ * calls to {@link #populate(ExtractorInput)}.
+ *
+ * @return the {@code PageHeader} of the last page read or an empty header if the packet has yet
+ * to be populated.
+ */
+ public OggPageHeader getPageHeader() {
+ return pageHeader;
+ }
+
+ /**
+ * Returns a {@link ParsableByteArray} containing the packet's payload.
+ */
+ public ParsableByteArray getPayload() {
+ return packetArray;
+ }
+
+ /**
+ * Trims the packet data array.
+ */
+ public void trimPayload() {
+ if (packetArray.data.length == OggPageHeader.MAX_PAGE_PAYLOAD) {
+ return;
+ }
+ packetArray.data = Arrays.copyOf(packetArray.data, Math.max(OggPageHeader.MAX_PAGE_PAYLOAD,
+ packetArray.limit()));
+ }
+
+ /**
+ * Calculates the size of the packet starting from {@code startSegmentIndex}.
+ *
+ * @param startSegmentIndex the index of the first segment of the packet.
+ * @return Size of the packet.
+ */
+ private int calculatePacketSize(int startSegmentIndex) {
+ segmentCount = 0;
+ int size = 0;
+ while (startSegmentIndex + segmentCount < pageHeader.pageSegmentCount) {
+ int segmentLength = pageHeader.laces[startSegmentIndex + segmentCount++];
+ size += segmentLength;
+ if (segmentLength != 255) {
+ // packets end at first lace < 255
+ break;
+ }
+ }
+ return size;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java
new file mode 100644
index 0000000000..afdccf80fd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java
@@ -0,0 +1,135 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Data object to store header information.
+ */
+/* package */ final class OggPageHeader {
+
+ public static final int EMPTY_PAGE_HEADER_SIZE = 27;
+ public static final int MAX_SEGMENT_COUNT = 255;
+ public static final int MAX_PAGE_PAYLOAD = 255 * 255;
+ public static final int MAX_PAGE_SIZE = EMPTY_PAGE_HEADER_SIZE + MAX_SEGMENT_COUNT
+ + MAX_PAGE_PAYLOAD;
+
+ private static final int TYPE_OGGS = 0x4f676753;
+
+ public int revision;
+ public int type;
+ /**
+ * The absolute granule position of the page. This is the total number of samples from the start
+ * of the file up to the <em>end</em> of the page. Samples partially in the page that continue on
+ * the next page do not count.
+ */
+ public long granulePosition;
+
+ public long streamSerialNumber;
+ public long pageSequenceNumber;
+ public long pageChecksum;
+ public int pageSegmentCount;
+ public int headerSize;
+ public int bodySize;
+ /**
+ * Be aware that {@code laces.length} is always {@link #MAX_SEGMENT_COUNT}. Instead use
+ * {@link #pageSegmentCount} to iterate.
+ */
+ public final int[] laces = new int[MAX_SEGMENT_COUNT];
+
+ private final ParsableByteArray scratch = new ParsableByteArray(MAX_SEGMENT_COUNT);
+
+ /**
+ * Resets all primitive member fields to zero.
+ */
+ public void reset() {
+ revision = 0;
+ type = 0;
+ granulePosition = 0;
+ streamSerialNumber = 0;
+ pageSequenceNumber = 0;
+ pageChecksum = 0;
+ pageSegmentCount = 0;
+ headerSize = 0;
+ bodySize = 0;
+ }
+
+ /**
+ * Peeks an Ogg page header and updates this {@link OggPageHeader}.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @param quiet Whether to return {@code false} rather than throwing an exception if the header
+ * cannot be populated.
+ * @return Whether the read was successful. The read fails if the end of the input is encountered
+ * without reading data.
+ * @throws IOException If reading data fails or the stream is invalid.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public boolean populate(ExtractorInput input, boolean quiet)
+ throws IOException, InterruptedException {
+ scratch.reset();
+ reset();
+ boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNSET
+ || input.getLength() - input.getPeekPosition() >= EMPTY_PAGE_HEADER_SIZE;
+ if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, EMPTY_PAGE_HEADER_SIZE, true)) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new EOFException();
+ }
+ }
+ if (scratch.readUnsignedInt() != TYPE_OGGS) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("expected OggS capture pattern at begin of page");
+ }
+ }
+
+ revision = scratch.readUnsignedByte();
+ if (revision != 0x00) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("unsupported bit stream revision");
+ }
+ }
+ type = scratch.readUnsignedByte();
+
+ granulePosition = scratch.readLittleEndianLong();
+ streamSerialNumber = scratch.readLittleEndianUnsignedInt();
+ pageSequenceNumber = scratch.readLittleEndianUnsignedInt();
+ pageChecksum = scratch.readLittleEndianUnsignedInt();
+ pageSegmentCount = scratch.readUnsignedByte();
+ headerSize = EMPTY_PAGE_HEADER_SIZE + pageSegmentCount;
+
+ // calculate total size of header including laces
+ scratch.reset();
+ input.peekFully(scratch.data, 0, pageSegmentCount);
+ for (int i = 0; i < pageSegmentCount; i++) {
+ laces[i] = scratch.readUnsignedByte();
+ bodySize += laces[i];
+ }
+
+ return true;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java
new file mode 100644
index 0000000000..0a0be963f7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java
@@ -0,0 +1,57 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import java.io.IOException;
+
+/**
+ * Used to seek in an Ogg stream. OggSeeker implementation may do direct seeking or progressive
+ * seeking. OggSeeker works together with a {@link SeekMap} instance to capture the queried position
+ * and start the seeking with an initial estimated position.
+ */
+/* package */ interface OggSeeker {
+
+ /**
+ * Returns a {@link SeekMap} that returns an initial estimated position for progressive seeking
+ * or the final position for direct seeking. Returns null if {@link #read} has yet to return -1.
+ */
+ SeekMap createSeekMap();
+
+ /**
+ * Starts a seek operation.
+ *
+ * @param targetGranule The target granule position.
+ */
+ void startSeek(long targetGranule);
+
+ /**
+ * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek.
+ * <p/>
+ * If more data is required or if the position of the input needs to be modified then a position
+ * from which data should be provided is returned. Else a negative value is returned. If a seek
+ * has been completed then the value returned is -(currentGranule + 2). Else it is -1.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @return A non-negative position to seek the {@link ExtractorInput} to, or -(currentGranule + 2)
+ * if the progressive seek has completed, or -1 otherwise.
+ * @throws IOException If reading from the {@link ExtractorInput} fails.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ long read(ExtractorInput input) throws IOException, InterruptedException;
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java
new file mode 100644
index 0000000000..c3f3a13d54
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java
@@ -0,0 +1,132 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * {@link StreamReader} to extract Opus data out of Ogg byte stream.
+ */
+/* package */ final class OpusReader extends StreamReader {
+
+ private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840;
+
+ /**
+ * Opus streams are always decoded at 48000 Hz.
+ */
+ private static final int SAMPLE_RATE = 48000;
+
+ private static final int OPUS_CODE = 0x4f707573;
+ private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
+
+ private boolean headerRead;
+
+ public static boolean verifyBitstreamType(ParsableByteArray data) {
+ if (data.bytesLeft() < OPUS_SIGNATURE.length) {
+ return false;
+ }
+ byte[] header = new byte[OPUS_SIGNATURE.length];
+ data.readBytes(header, 0, OPUS_SIGNATURE.length);
+ return Arrays.equals(header, OPUS_SIGNATURE);
+ }
+
+ @Override
+ protected void reset(boolean headerData) {
+ super.reset(headerData);
+ if (headerData) {
+ headerRead = false;
+ }
+ }
+
+ @Override
+ protected long preparePayload(ParsableByteArray packet) {
+ return convertTimeToGranule(getPacketDurationUs(packet.data));
+ }
+
+ @Override
+ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
+ if (!headerRead) {
+ byte[] metadata = Arrays.copyOf(packet.data, packet.limit());
+ int channelCount = metadata[9] & 0xFF;
+ int preskip = ((metadata[11] & 0xFF) << 8) | (metadata[10] & 0xFF);
+
+ List<byte[]> initializationData = new ArrayList<>(3);
+ initializationData.add(metadata);
+ putNativeOrderLong(initializationData, preskip);
+ putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES);
+
+ setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, null,
+ Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0,
+ null);
+ headerRead = true;
+ } else {
+ boolean headerPacket = packet.readInt() == OPUS_CODE;
+ packet.setPosition(0);
+ return headerPacket;
+ }
+ return true;
+ }
+
+ private void putNativeOrderLong(List<byte[]> initializationData, int samples) {
+ long ns = (samples * C.NANOS_PER_SECOND) / SAMPLE_RATE;
+ byte[] array = ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(ns).array();
+ initializationData.add(array);
+ }
+
+ /**
+ * Returns the duration of the given audio packet.
+ *
+ * @param packet Contains audio data.
+ * @return Returns the duration of the given audio packet.
+ */
+ private long getPacketDurationUs(byte[] packet) {
+ int toc = packet[0] & 0xFF;
+ int frames;
+ switch (toc & 0x3) {
+ case 0:
+ frames = 1;
+ break;
+ case 1:
+ case 2:
+ frames = 2;
+ break;
+ default:
+ frames = packet[1] & 0x3F;
+ break;
+ }
+
+ int config = toc >> 3;
+ int length = config & 0x3;
+ if (config >= 16) {
+ length = 2500 << length;
+ } else if (config >= 12) {
+ length = 10000 << (length & 0x1);
+ } else if (length == 3) {
+ length = 60000;
+ } else {
+ length = 10000 << length;
+ }
+ return (long) frames * length;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
new file mode 100644
index 0000000000..067c8aef03
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
@@ -0,0 +1,268 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/** StreamReader abstract class. */
+@SuppressWarnings("UngroupedOverloads")
+/* package */ abstract class StreamReader {
+
+ private static final int STATE_READ_HEADERS = 0;
+ private static final int STATE_SKIP_HEADERS = 1;
+ private static final int STATE_READ_PAYLOAD = 2;
+ private static final int STATE_END_OF_INPUT = 3;
+
+ static class SetupData {
+ Format format;
+ OggSeeker oggSeeker;
+ }
+
+ private final OggPacket oggPacket;
+
+ private TrackOutput trackOutput;
+ private ExtractorOutput extractorOutput;
+ private OggSeeker oggSeeker;
+ private long targetGranule;
+ private long payloadStartPosition;
+ private long currentGranule;
+ private int state;
+ private int sampleRate;
+ private SetupData setupData;
+ private long lengthOfReadPacket;
+ private boolean seekMapSet;
+ private boolean formatSet;
+
+ public StreamReader() {
+ oggPacket = new OggPacket();
+ }
+
+ void init(ExtractorOutput output, TrackOutput trackOutput) {
+ this.extractorOutput = output;
+ this.trackOutput = trackOutput;
+ reset(true);
+ }
+
+ /**
+ * Resets the state of the {@link StreamReader}.
+ *
+ * @param headerData Resets parsed header data too.
+ */
+ protected void reset(boolean headerData) {
+ if (headerData) {
+ setupData = new SetupData();
+ payloadStartPosition = 0;
+ state = STATE_READ_HEADERS;
+ } else {
+ state = STATE_SKIP_HEADERS;
+ }
+ targetGranule = -1;
+ currentGranule = 0;
+ }
+
+ /**
+ * @see Extractor#seek(long, long)
+ */
+ final void seek(long position, long timeUs) {
+ oggPacket.reset();
+ if (position == 0) {
+ reset(!seekMapSet);
+ } else {
+ if (state != STATE_READ_HEADERS) {
+ targetGranule = convertTimeToGranule(timeUs);
+ oggSeeker.startSeek(targetGranule);
+ state = STATE_READ_PAYLOAD;
+ }
+ }
+ }
+
+ /**
+ * @see Extractor#read(ExtractorInput, PositionHolder)
+ */
+ final int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ switch (state) {
+ case STATE_READ_HEADERS:
+ return readHeaders(input);
+ case STATE_SKIP_HEADERS:
+ input.skipFully((int) payloadStartPosition);
+ state = STATE_READ_PAYLOAD;
+ return Extractor.RESULT_CONTINUE;
+ case STATE_READ_PAYLOAD:
+ return readPayload(input, seekPosition);
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ }
+
+ private int readHeaders(ExtractorInput input) throws IOException, InterruptedException {
+ boolean readingHeaders = true;
+ while (readingHeaders) {
+ if (!oggPacket.populate(input)) {
+ state = STATE_END_OF_INPUT;
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ lengthOfReadPacket = input.getPosition() - payloadStartPosition;
+
+ readingHeaders = readHeaders(oggPacket.getPayload(), payloadStartPosition, setupData);
+ if (readingHeaders) {
+ payloadStartPosition = input.getPosition();
+ }
+ }
+
+ sampleRate = setupData.format.sampleRate;
+ if (!formatSet) {
+ trackOutput.format(setupData.format);
+ formatSet = true;
+ }
+
+ if (setupData.oggSeeker != null) {
+ oggSeeker = setupData.oggSeeker;
+ } else if (input.getLength() == C.LENGTH_UNSET) {
+ oggSeeker = new UnseekableOggSeeker();
+ } else {
+ OggPageHeader firstPayloadPageHeader = oggPacket.getPageHeader();
+ boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream.
+ oggSeeker =
+ new DefaultOggSeeker(
+ this,
+ payloadStartPosition,
+ input.getLength(),
+ firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize,
+ firstPayloadPageHeader.granulePosition,
+ isLastPage);
+ }
+
+ setupData = null;
+ state = STATE_READ_PAYLOAD;
+ // First payload packet. Trim the payload array of the ogg packet after headers have been read.
+ oggPacket.trimPayload();
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private int readPayload(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ long position = oggSeeker.read(input);
+ if (position >= 0) {
+ seekPosition.position = position;
+ return Extractor.RESULT_SEEK;
+ } else if (position < -1) {
+ onSeekEnd(-(position + 2));
+ }
+ if (!seekMapSet) {
+ SeekMap seekMap = oggSeeker.createSeekMap();
+ extractorOutput.seekMap(seekMap);
+ seekMapSet = true;
+ }
+
+ if (lengthOfReadPacket > 0 || oggPacket.populate(input)) {
+ lengthOfReadPacket = 0;
+ ParsableByteArray payload = oggPacket.getPayload();
+ long granulesInPacket = preparePayload(payload);
+ if (granulesInPacket >= 0 && currentGranule + granulesInPacket >= targetGranule) {
+ // calculate time and send payload data to codec
+ long timeUs = convertGranuleToTime(currentGranule);
+ trackOutput.sampleData(payload, payload.limit());
+ trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, payload.limit(), 0, null);
+ targetGranule = -1;
+ }
+ currentGranule += granulesInPacket;
+ } else {
+ state = STATE_END_OF_INPUT;
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ /**
+ * Converts granule value to time.
+ *
+ * @param granule The granule value.
+ * @return Time in milliseconds.
+ */
+ protected long convertGranuleToTime(long granule) {
+ return (granule * C.MICROS_PER_SECOND) / sampleRate;
+ }
+
+ /**
+ * Converts time value to granule.
+ *
+ * @param timeUs Time in milliseconds.
+ * @return The granule value.
+ */
+ protected long convertTimeToGranule(long timeUs) {
+ return (sampleRate * timeUs) / C.MICROS_PER_SECOND;
+ }
+
+ /**
+ * Prepares payload data in the packet for submitting to TrackOutput and returns number of
+ * granules in the packet.
+ *
+ * @param packet Ogg payload data packet.
+ * @return Number of granules in the packet or -1 if the packet doesn't contain payload data.
+ */
+ protected abstract long preparePayload(ParsableByteArray packet);
+
+ /**
+ * Checks if the given packet is a header packet and reads it.
+ *
+ * @param packet An ogg packet.
+ * @param position Position of the given header packet.
+ * @param setupData Setup data to be filled.
+ * @return Whether the packet contains header data.
+ */
+ protected abstract boolean readHeaders(ParsableByteArray packet, long position,
+ SetupData setupData) throws IOException, InterruptedException;
+
+ /**
+ * Called on end of seeking.
+ *
+ * @param currentGranule The granule at the current input position.
+ */
+ protected void onSeekEnd(long currentGranule) {
+ this.currentGranule = currentGranule;
+ }
+
+ private static final class UnseekableOggSeeker implements OggSeeker {
+
+ @Override
+ public long read(ExtractorInput input) {
+ return -1;
+ }
+
+ @Override
+ public void startSeek(long targetGranule) {
+ // Do nothing.
+ }
+
+ @Override
+ public SeekMap createSeekMap() {
+ return new SeekMap.Unseekable(C.TIME_UNSET);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java
new file mode 100644
index 0000000000..cb0678a285
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java
@@ -0,0 +1,198 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg;
+
+import androidx.annotation.VisibleForTesting;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil.Mode;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * {@link StreamReader} to extract Vorbis data out of Ogg byte stream.
+ */
+/* package */ final class VorbisReader extends StreamReader {
+
+ private VorbisSetup vorbisSetup;
+ private int previousPacketBlockSize;
+ private boolean seenFirstAudioPacket;
+
+ private VorbisUtil.VorbisIdHeader vorbisIdHeader;
+ private VorbisUtil.CommentHeader commentHeader;
+
+ public static boolean verifyBitstreamType(ParsableByteArray data) {
+ try {
+ return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true);
+ } catch (ParserException e) {
+ return false;
+ }
+ }
+
+ @Override
+ protected void reset(boolean headerData) {
+ super.reset(headerData);
+ if (headerData) {
+ vorbisSetup = null;
+ vorbisIdHeader = null;
+ commentHeader = null;
+ }
+ previousPacketBlockSize = 0;
+ seenFirstAudioPacket = false;
+ }
+
+ @Override
+ protected void onSeekEnd(long currentGranule) {
+ super.onSeekEnd(currentGranule);
+ seenFirstAudioPacket = currentGranule != 0;
+ previousPacketBlockSize = vorbisIdHeader != null ? vorbisIdHeader.blockSize0 : 0;
+ }
+
+ @Override
+ protected long preparePayload(ParsableByteArray packet) {
+ // if this is not an audio packet...
+ if ((packet.data[0] & 0x01) == 1) {
+ return -1;
+ }
+
+ // ... we need to decode the block size
+ int packetBlockSize = decodeBlockSize(packet.data[0], vorbisSetup);
+ // a packet contains samples produced from overlapping the previous and current frame data
+ // (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)
+ int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4
+ : 0;
+ // codec expects the number of samples appended to audio data
+ appendNumberOfSamples(packet, samplesInPacket);
+
+ // update state in members for next iteration
+ seenFirstAudioPacket = true;
+ previousPacketBlockSize = packetBlockSize;
+ return samplesInPacket;
+ }
+
+ @Override
+ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
+ throws IOException, InterruptedException {
+ if (vorbisSetup != null) {
+ return false;
+ }
+
+ vorbisSetup = readSetupHeaders(packet);
+ if (vorbisSetup == null) {
+ return true;
+ }
+
+ ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
+ codecInitialisationData.add(vorbisSetup.idHeader.data);
+ codecInitialisationData.add(vorbisSetup.setupHeaderData);
+
+ setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null,
+ this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE,
+ this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,
+ codecInitialisationData, null, 0, null);
+ return true;
+ }
+
+ @VisibleForTesting
+ /* package */ VorbisSetup readSetupHeaders(ParsableByteArray scratch) throws IOException {
+
+ if (vorbisIdHeader == null) {
+ vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch);
+ return null;
+ }
+
+ if (commentHeader == null) {
+ commentHeader = VorbisUtil.readVorbisCommentHeader(scratch);
+ return null;
+ }
+
+ // the third packet contains the setup header
+ byte[] setupHeaderData = new byte[scratch.limit()];
+ // raw data of vorbis setup header has to be passed to decoder as CSD buffer #2
+ System.arraycopy(scratch.data, 0, setupHeaderData, 0, scratch.limit());
+ // partially decode setup header to get the modes
+ Mode[] modes = VorbisUtil.readVorbisModes(scratch, vorbisIdHeader.channels);
+ // we need the ilog of modes all the time when extracting, so we compute it once
+ int iLogModes = VorbisUtil.iLog(modes.length - 1);
+
+ return new VorbisSetup(vorbisIdHeader, commentHeader, setupHeaderData, modes, iLogModes);
+ }
+
+ /**
+ * Reads an int of {@code length} bits from {@code src} starting at {@code
+ * leastSignificantBitIndex}.
+ *
+ * @param src the {@code byte} to read from.
+ * @param length the length in bits of the int to read.
+ * @param leastSignificantBitIndex the index of the least significant bit of the int to read.
+ * @return the int value read.
+ */
+ @VisibleForTesting
+ /* package */ static int readBits(byte src, int length, int leastSignificantBitIndex) {
+ return (src >> leastSignificantBitIndex) & (255 >>> (8 - length));
+ }
+
+ @VisibleForTesting
+ /* package */ static void appendNumberOfSamples(
+ ParsableByteArray buffer, long packetSampleCount) {
+
+ buffer.setLimit(buffer.limit() + 4);
+ // The vorbis decoder expects the number of samples in the packet
+ // to be appended to the audio data as an int32
+ buffer.data[buffer.limit() - 4] = (byte) (packetSampleCount & 0xFF);
+ buffer.data[buffer.limit() - 3] = (byte) ((packetSampleCount >>> 8) & 0xFF);
+ buffer.data[buffer.limit() - 2] = (byte) ((packetSampleCount >>> 16) & 0xFF);
+ buffer.data[buffer.limit() - 1] = (byte) ((packetSampleCount >>> 24) & 0xFF);
+ }
+
+ private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) {
+ // read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1)
+ int modeNumber = readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1);
+ int currentBlockSize;
+ if (!vorbisSetup.modes[modeNumber].blockFlag) {
+ currentBlockSize = vorbisSetup.idHeader.blockSize0;
+ } else {
+ currentBlockSize = vorbisSetup.idHeader.blockSize1;
+ }
+ return currentBlockSize;
+ }
+
+ /**
+ * Class to hold all data read from Vorbis setup headers.
+ */
+ /* package */ static final class VorbisSetup {
+
+ public final VorbisUtil.VorbisIdHeader idHeader;
+ public final VorbisUtil.CommentHeader commentHeader;
+ public final byte[] setupHeaderData;
+ public final Mode[] modes;
+ public final int iLogModes;
+
+ public VorbisSetup(VorbisUtil.VorbisIdHeader idHeader, VorbisUtil.CommentHeader
+ commentHeader, byte[] setupHeaderData, Mode[] modes, int iLogModes) {
+ this.idHeader = idHeader;
+ this.commentHeader = commentHeader;
+ this.setupHeaderData = setupHeaderData;
+ this.modes = modes;
+ this.iLogModes = iLogModes;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java
new file mode 100644
index 0000000000..a7b32782ff
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java
@@ -0,0 +1,170 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.rawcc;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * Extracts data from the RawCC container format.
+ */
+public final class RawCcExtractor implements Extractor {
+
+ private static final int SCRATCH_SIZE = 9;
+ private static final int HEADER_SIZE = 8;
+ private static final int HEADER_ID = 0x52434301;
+ private static final int TIMESTAMP_SIZE_V0 = 4;
+ private static final int TIMESTAMP_SIZE_V1 = 8;
+
+ // Parser states.
+ private static final int STATE_READING_HEADER = 0;
+ private static final int STATE_READING_TIMESTAMP_AND_COUNT = 1;
+ private static final int STATE_READING_SAMPLES = 2;
+
+ private final Format format;
+
+ private final ParsableByteArray dataScratch;
+
+ private TrackOutput trackOutput;
+
+ private int parserState;
+ private int version;
+ private long timestampUs;
+ private int remainingSampleCount;
+ private int sampleBytesWritten;
+
+ public RawCcExtractor(Format format) {
+ this.format = format;
+ dataScratch = new ParsableByteArray(SCRATCH_SIZE);
+ parserState = STATE_READING_HEADER;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ trackOutput = output.track(0, C.TRACK_TYPE_TEXT);
+ output.endTracks();
+ trackOutput.format(format);
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ dataScratch.reset();
+ input.peekFully(dataScratch.data, 0, HEADER_SIZE);
+ return dataScratch.readInt() == HEADER_ID;
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ while (true) {
+ switch (parserState) {
+ case STATE_READING_HEADER:
+ if (parseHeader(input)) {
+ parserState = STATE_READING_TIMESTAMP_AND_COUNT;
+ } else {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_TIMESTAMP_AND_COUNT:
+ if (parseTimestampAndSampleCount(input)) {
+ parserState = STATE_READING_SAMPLES;
+ } else {
+ parserState = STATE_READING_HEADER;
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_SAMPLES:
+ parseSamples(input);
+ parserState = STATE_READING_TIMESTAMP_AND_COUNT;
+ return RESULT_CONTINUE;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ parserState = STATE_READING_HEADER;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException {
+ dataScratch.reset();
+ if (input.readFully(dataScratch.data, 0, HEADER_SIZE, true)) {
+ if (dataScratch.readInt() != HEADER_ID) {
+ throw new IOException("Input not RawCC");
+ }
+ version = dataScratch.readUnsignedByte();
+ // no versions use the flag fields yet
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException,
+ InterruptedException {
+ dataScratch.reset();
+ if (version == 0) {
+ if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V0 + 1, true)) {
+ return false;
+ }
+ // version 0 timestamps are 45kHz, so we need to convert them into us
+ timestampUs = dataScratch.readUnsignedInt() * 1000 / 45;
+ } else if (version == 1) {
+ if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V1 + 1, true)) {
+ return false;
+ }
+ timestampUs = dataScratch.readLong();
+ } else {
+ throw new ParserException("Unsupported version number: " + version);
+ }
+
+ remainingSampleCount = dataScratch.readUnsignedByte();
+ sampleBytesWritten = 0;
+ return true;
+ }
+
+ private void parseSamples(ExtractorInput input) throws IOException, InterruptedException {
+ for (; remainingSampleCount > 0; remainingSampleCount--) {
+ dataScratch.reset();
+ input.readFully(dataScratch.data, 0, 3);
+
+ trackOutput.sampleData(dataScratch, 3);
+ sampleBytesWritten += 3;
+ }
+
+ if (sampleBytesWritten > 0) {
+ trackOutput.sampleMetadata(timestampUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null);
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
new file mode 100644
index 0000000000..a0a1365935
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
@@ -0,0 +1,149 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * Extracts data from (E-)AC-3 bitstreams.
+ */
+public final class Ac3Extractor implements Extractor {
+
+ /** Factory for {@link Ac3Extractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Ac3Extractor()};
+
+ /**
+ * The maximum number of bytes to search when sniffing, excluding ID3 information, before giving
+ * up.
+ */
+ private static final int MAX_SNIFF_BYTES = 8 * 1024;
+ private static final int AC3_SYNC_WORD = 0x0B77;
+ private static final int MAX_SYNC_FRAME_SIZE = 2786;
+
+ private final Ac3Reader reader;
+ private final ParsableByteArray sampleData;
+
+ private boolean startedPacket;
+
+ /** Creates a new extractor for AC-3 bitstreams. */
+ public Ac3Extractor() {
+ reader = new Ac3Reader();
+ sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE);
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ // Skip any ID3 headers.
+ ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH);
+ int startPosition = 0;
+ while (true) {
+ input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH);
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != ID3_TAG) {
+ break;
+ }
+ scratch.skipBytes(3); // version, flags
+ int length = scratch.readSynchSafeInt();
+ startPosition += 10 + length;
+ input.advancePeekPosition(length);
+ }
+ input.resetPeekPosition();
+ input.advancePeekPosition(startPosition);
+
+ int headerPosition = startPosition;
+ int validFramesCount = 0;
+ while (true) {
+ input.peekFully(scratch.data, 0, 6);
+ scratch.setPosition(0);
+ int syncBytes = scratch.readUnsignedShort();
+ if (syncBytes != AC3_SYNC_WORD) {
+ validFramesCount = 0;
+ input.resetPeekPosition();
+ if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {
+ return false;
+ }
+ input.advancePeekPosition(headerPosition);
+ } else {
+ if (++validFramesCount >= 4) {
+ return true;
+ }
+ int frameSize = Ac3Util.parseAc3SyncframeSize(scratch.data);
+ if (frameSize == C.LENGTH_UNSET) {
+ return false;
+ }
+ input.advancePeekPosition(frameSize - 6);
+ }
+ }
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ reader.createTracks(output, new TrackIdGenerator(0, 1));
+ output.endTracks();
+ output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ startedPacket = false;
+ reader.seek();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing.
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
+ InterruptedException {
+ int bytesRead = input.read(sampleData.data, 0, MAX_SYNC_FRAME_SIZE);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ // Feed whatever data we have to the reader, regardless of whether the read finished or not.
+ sampleData.setPosition(0);
+ sampleData.setLimit(bytesRead);
+
+ if (!startedPacket) {
+ // Pass data to the reader as though it's contained within a single infinitely long packet.
+ reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR);
+ startedPacket = true;
+ }
+ // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes
+ // unnecessary to copy the data through packetBuffer.
+ reader.consume(sampleData);
+ return RESULT_CONTINUE;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java
new file mode 100644
index 0000000000..3a6eebbcd2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java
@@ -0,0 +1,209 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Parses a continuous (E-)AC-3 byte stream and extracts individual samples.
+ */
+public final class Ac3Reader implements ElementaryStreamReader {
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATE_FINDING_SYNC, STATE_READING_HEADER, STATE_READING_SAMPLE})
+ private @interface State {}
+
+ private static final int STATE_FINDING_SYNC = 0;
+ private static final int STATE_READING_HEADER = 1;
+ private static final int STATE_READING_SAMPLE = 2;
+
+ private static final int HEADER_SIZE = 128;
+
+ private final ParsableBitArray headerScratchBits;
+ private final ParsableByteArray headerScratchBytes;
+ private final String language;
+
+ private String trackFormatId;
+ private TrackOutput output;
+
+ @State private int state;
+ private int bytesRead;
+
+ // Used to find the header.
+ private boolean lastByteWas0B;
+
+ // Used when parsing the header.
+ private long sampleDurationUs;
+ private Format format;
+ private int sampleSize;
+
+ // Used when reading the samples.
+ private long timeUs;
+
+ /**
+ * Constructs a new reader for (E-)AC-3 elementary streams.
+ */
+ public Ac3Reader() {
+ this(null);
+ }
+
+ /**
+ * Constructs a new reader for (E-)AC-3 elementary streams.
+ *
+ * @param language Track language.
+ */
+ public Ac3Reader(String language) {
+ headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]);
+ headerScratchBytes = new ParsableByteArray(headerScratchBits.data);
+ state = STATE_FINDING_SYNC;
+ this.language = language;
+ }
+
+ @Override
+ public void seek() {
+ state = STATE_FINDING_SYNC;
+ bytesRead = 0;
+ lastByteWas0B = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) {
+ generator.generateNewId();
+ trackFormatId = generator.getFormatId();
+ output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ timeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_SYNC:
+ if (skipToNextSync(data)) {
+ state = STATE_READING_HEADER;
+ headerScratchBytes.data[0] = 0x0B;
+ headerScratchBytes.data[1] = 0x77;
+ bytesRead = 2;
+ }
+ break;
+ case STATE_READING_HEADER:
+ if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) {
+ parseHeader();
+ headerScratchBytes.setPosition(0);
+ output.sampleData(headerScratchBytes, HEADER_SIZE);
+ state = STATE_READING_SAMPLE;
+ }
+ break;
+ case STATE_READING_SAMPLE:
+ int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
+ output.sampleData(data, bytesToRead);
+ bytesRead += bytesToRead;
+ if (bytesRead == sampleSize) {
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ timeUs += sampleDurationUs;
+ state = STATE_FINDING_SYNC;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Continues a read from the provided {@code source} into a given {@code target}. It's assumed
+ * that the data should be written into {@code target} starting from an offset of zero.
+ *
+ * @param source The source from which to read.
+ * @param target The target into which data is to be read.
+ * @param targetLength The target length of the read.
+ * @return Whether the target length was reached.
+ */
+ private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
+ int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
+ source.readBytes(target, bytesRead, bytesToRead);
+ bytesRead += bytesToRead;
+ return bytesRead == targetLength;
+ }
+
+ /**
+ * Locates the next syncword, advancing the position to the byte that immediately follows it. If a
+ * syncword was not located, the position is advanced to the limit.
+ *
+ * @param pesBuffer The buffer whose position should be advanced.
+ * @return Whether a syncword position was found.
+ */
+ private boolean skipToNextSync(ParsableByteArray pesBuffer) {
+ while (pesBuffer.bytesLeft() > 0) {
+ if (!lastByteWas0B) {
+ lastByteWas0B = pesBuffer.readUnsignedByte() == 0x0B;
+ continue;
+ }
+ int secondByte = pesBuffer.readUnsignedByte();
+ if (secondByte == 0x77) {
+ lastByteWas0B = false;
+ return true;
+ } else {
+ lastByteWas0B = secondByte == 0x0B;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Parses the sample header.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private void parseHeader() {
+ headerScratchBits.setPosition(0);
+ SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits);
+ if (format == null || frameInfo.channelCount != format.channelCount
+ || frameInfo.sampleRate != format.sampleRate
+ || frameInfo.mimeType != format.sampleMimeType) {
+ format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null,
+ Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null,
+ null, 0, language);
+ output.format(format);
+ }
+ sampleSize = frameInfo.frameSize;
+ // In this class a sample is an access unit (syncframe in AC-3), but Format#sampleRate
+ // specifies the number of PCM audio samples per second.
+ sampleDurationUs = C.MICROS_PER_SECOND * frameInfo.sampleCount / format.sampleRate;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java
new file mode 100644
index 0000000000..9578d110b7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util.AC40_SYNCWORD;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util.AC41_SYNCWORD;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/** Extracts data from AC-4 bitstreams. */
+public final class Ac4Extractor implements Extractor {
+
+ /** Factory for {@link Ac4Extractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Ac4Extractor()};
+
+ /**
+ * The maximum number of bytes to search when sniffing, excluding ID3 information, before giving
+ * up.
+ */
+ private static final int MAX_SNIFF_BYTES = 8 * 1024;
+
+ /**
+ * The size of the reading buffer, in bytes. This value is determined based on the maximum frame
+ * size used in broadcast applications.
+ */
+ private static final int READ_BUFFER_SIZE = 16384;
+
+ /** The size of the frame header, in bytes. */
+ private static final int FRAME_HEADER_SIZE = 7;
+
+ private final Ac4Reader reader;
+ private final ParsableByteArray sampleData;
+
+ private boolean startedPacket;
+
+ /** Creates a new extractor for AC-4 bitstreams. */
+ public Ac4Extractor() {
+ reader = new Ac4Reader();
+ sampleData = new ParsableByteArray(READ_BUFFER_SIZE);
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ // Skip any ID3 headers.
+ ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH);
+ int startPosition = 0;
+ while (true) {
+ input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH);
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != ID3_TAG) {
+ break;
+ }
+ scratch.skipBytes(3); // version, flags
+ int length = scratch.readSynchSafeInt();
+ startPosition += 10 + length;
+ input.advancePeekPosition(length);
+ }
+ input.resetPeekPosition();
+ input.advancePeekPosition(startPosition);
+
+ int headerPosition = startPosition;
+ int validFramesCount = 0;
+ while (true) {
+ input.peekFully(scratch.data, /* offset= */ 0, /* length= */ FRAME_HEADER_SIZE);
+ scratch.setPosition(0);
+ int syncBytes = scratch.readUnsignedShort();
+ if (syncBytes != AC40_SYNCWORD && syncBytes != AC41_SYNCWORD) {
+ validFramesCount = 0;
+ input.resetPeekPosition();
+ if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {
+ return false;
+ }
+ input.advancePeekPosition(headerPosition);
+ } else {
+ if (++validFramesCount >= 4) {
+ return true;
+ }
+ int frameSize = Ac4Util.parseAc4SyncframeSize(scratch.data, syncBytes);
+ if (frameSize == C.LENGTH_UNSET) {
+ return false;
+ }
+ input.advancePeekPosition(frameSize - FRAME_HEADER_SIZE);
+ }
+ }
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ reader.createTracks(
+ output, new TrackIdGenerator(/* firstTrackId= */ 0, /* trackIdIncrement= */ 1));
+ output.endTracks();
+ output.seekMap(new SeekMap.Unseekable(/* durationUs= */ C.TIME_UNSET));
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ startedPacket = false;
+ reader.seek();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing.
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ int bytesRead = input.read(sampleData.data, /* offset= */ 0, /* length= */ READ_BUFFER_SIZE);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ // Feed whatever data we have to the reader, regardless of whether the read finished or not.
+ sampleData.setPosition(0);
+ sampleData.setLimit(bytesRead);
+
+ if (!startedPacket) {
+ // Pass data to the reader as though it's contained within a single infinitely long packet.
+ reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR);
+ startedPacket = true;
+ }
+ // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes
+ // unnecessary to copy the data through packetBuffer.
+ reader.consume(sampleData);
+ return RESULT_CONTINUE;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java
new file mode 100644
index 0000000000..2b9965b19b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util.SyncFrameInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Parses a continuous AC-4 byte stream and extracts individual samples. */
+public final class Ac4Reader implements ElementaryStreamReader {
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATE_FINDING_SYNC, STATE_READING_HEADER, STATE_READING_SAMPLE})
+ private @interface State {}
+
+ private static final int STATE_FINDING_SYNC = 0;
+ private static final int STATE_READING_HEADER = 1;
+ private static final int STATE_READING_SAMPLE = 2;
+
+ private final ParsableBitArray headerScratchBits;
+ private final ParsableByteArray headerScratchBytes;
+ private final String language;
+
+ private String trackFormatId;
+ private TrackOutput output;
+
+ @State private int state;
+ private int bytesRead;
+
+ // Used to find the header.
+ private boolean lastByteWasAC;
+ private boolean hasCRC;
+
+ // Used when parsing the header.
+ private long sampleDurationUs;
+ private Format format;
+ private int sampleSize;
+
+ // Used when reading the samples.
+ private long timeUs;
+
+ /** Constructs a new reader for AC-4 elementary streams. */
+ public Ac4Reader() {
+ this(null);
+ }
+
+ /**
+ * Constructs a new reader for AC-4 elementary streams.
+ *
+ * @param language Track language.
+ */
+ public Ac4Reader(String language) {
+ headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]);
+ headerScratchBytes = new ParsableByteArray(headerScratchBits.data);
+ state = STATE_FINDING_SYNC;
+ bytesRead = 0;
+ lastByteWasAC = false;
+ hasCRC = false;
+ this.language = language;
+ }
+
+ @Override
+ public void seek() {
+ state = STATE_FINDING_SYNC;
+ bytesRead = 0;
+ lastByteWasAC = false;
+ hasCRC = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) {
+ generator.generateNewId();
+ trackFormatId = generator.getFormatId();
+ output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ timeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_SYNC:
+ if (skipToNextSync(data)) {
+ state = STATE_READING_HEADER;
+ headerScratchBytes.data[0] = (byte) 0xAC;
+ headerScratchBytes.data[1] = (byte) (hasCRC ? 0x41 : 0x40);
+ bytesRead = 2;
+ }
+ break;
+ case STATE_READING_HEADER:
+ if (continueRead(data, headerScratchBytes.data, Ac4Util.HEADER_SIZE_FOR_PARSER)) {
+ parseHeader();
+ headerScratchBytes.setPosition(0);
+ output.sampleData(headerScratchBytes, Ac4Util.HEADER_SIZE_FOR_PARSER);
+ state = STATE_READING_SAMPLE;
+ }
+ break;
+ case STATE_READING_SAMPLE:
+ int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
+ output.sampleData(data, bytesToRead);
+ bytesRead += bytesToRead;
+ if (bytesRead == sampleSize) {
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ timeUs += sampleDurationUs;
+ state = STATE_FINDING_SYNC;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Continues a read from the provided {@code source} into a given {@code target}. It's assumed
+ * that the data should be written into {@code target} starting from an offset of zero.
+ *
+ * @param source The source from which to read.
+ * @param target The target into which data is to be read.
+ * @param targetLength The target length of the read.
+ * @return Whether the target length was reached.
+ */
+ private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
+ int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
+ source.readBytes(target, bytesRead, bytesToRead);
+ bytesRead += bytesToRead;
+ return bytesRead == targetLength;
+ }
+
+ /**
+ * Locates the next syncword, advancing the position to the byte that immediately follows it. If a
+ * syncword was not located, the position is advanced to the limit.
+ *
+ * @param pesBuffer The buffer whose position should be advanced.
+ * @return Whether a syncword position was found.
+ */
+ private boolean skipToNextSync(ParsableByteArray pesBuffer) {
+ while (pesBuffer.bytesLeft() > 0) {
+ if (!lastByteWasAC) {
+ lastByteWasAC = (pesBuffer.readUnsignedByte() == 0xAC);
+ continue;
+ }
+ int secondByte = pesBuffer.readUnsignedByte();
+ lastByteWasAC = secondByte == 0xAC;
+ if (secondByte == 0x40 || secondByte == 0x41) {
+ hasCRC = secondByte == 0x41;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Parses the sample header. */
+ @SuppressWarnings("ReferenceEquality")
+ private void parseHeader() {
+ headerScratchBits.setPosition(0);
+ SyncFrameInfo frameInfo = Ac4Util.parseAc4SyncframeInfo(headerScratchBits);
+ if (format == null
+ || frameInfo.channelCount != format.channelCount
+ || frameInfo.sampleRate != format.sampleRate
+ || !MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) {
+ format =
+ Format.createAudioSampleFormat(
+ trackFormatId,
+ MimeTypes.AUDIO_AC4,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ /* maxInputSize= */ Format.NO_VALUE,
+ frameInfo.channelCount,
+ frameInfo.sampleRate,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ language);
+ output.format(format);
+ }
+ sampleSize = frameInfo.frameSize;
+ // In this class a sample is an AC-4 sync frame, but Format#sampleRate specifies the number of
+ // PCM audio samples per second.
+ sampleDurationUs = C.MICROS_PER_SECOND * frameInfo.sampleCount / format.sampleRate;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
new file mode 100644
index 0000000000..b91abfc75a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
@@ -0,0 +1,332 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Extracts data from AAC bit streams with ADTS framing.
+ */
+public final class AdtsExtractor implements Extractor {
+
+ /** Factory for {@link AdtsExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AdtsExtractor()};
+
+ /**
+ * Flags controlling the behavior of the extractor. Possible flag value is {@link
+ * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING})
+ public @interface Flags {}
+ /**
+ * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would
+ * otherwise not be possible.
+ *
+ * <p>Note that this approach may result in approximated stream duration and seek position that
+ * are not precise, especially when the stream bitrate varies a lot.
+ */
+ public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
+
+ private static final int MAX_PACKET_SIZE = 2 * 1024;
+ /**
+ * The maximum number of bytes to search when sniffing, excluding the header, before giving up.
+ * Frame sizes are represented by 13-bit fields, so expect a valid frame in the first 8192 bytes.
+ */
+ private static final int MAX_SNIFF_BYTES = 8 * 1024;
+ /**
+ * The maximum number of frames to use when calculating the average frame size for constant
+ * bitrate seeking.
+ */
+ private static final int NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE = 1000;
+
+ private final @Flags int flags;
+
+ private final AdtsReader reader;
+ private final ParsableByteArray packetBuffer;
+ private final ParsableByteArray scratch;
+ private final ParsableBitArray scratchBits;
+
+ @Nullable private ExtractorOutput extractorOutput;
+
+ private long firstSampleTimestampUs;
+ private long firstFramePosition;
+ private int averageFrameSize;
+ private boolean hasCalculatedAverageFrameSize;
+ private boolean startedPacket;
+ private boolean hasOutputSeekMap;
+
+ /** Creates a new extractor for ADTS bitstreams. */
+ public AdtsExtractor() {
+ this(/* flags= */ 0);
+ }
+
+ /**
+ * Creates a new extractor for ADTS bitstreams.
+ *
+ * @param flags Flags that control the extractor's behavior.
+ */
+ public AdtsExtractor(@Flags int flags) {
+ this.flags = flags;
+ reader = new AdtsReader(true);
+ packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
+ averageFrameSize = C.LENGTH_UNSET;
+ firstFramePosition = C.POSITION_UNSET;
+ // Allocate scratch space for an ID3 header. The same buffer is also used to read 4 byte values.
+ scratch = new ParsableByteArray(ID3_HEADER_LENGTH);
+ scratchBits = new ParsableBitArray(scratch.data);
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ // Skip any ID3 headers.
+ int startPosition = peekId3Header(input);
+
+ // Try to find four or more consecutive AAC audio frames, exceeding the MPEG TS packet size.
+ int headerPosition = startPosition;
+ int totalValidFramesSize = 0;
+ int validFramesCount = 0;
+ while (true) {
+ input.peekFully(scratch.data, 0, 2);
+ scratch.setPosition(0);
+ int syncBytes = scratch.readUnsignedShort();
+ if (!AdtsReader.isAdtsSyncWord(syncBytes)) {
+ validFramesCount = 0;
+ totalValidFramesSize = 0;
+ input.resetPeekPosition();
+ if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {
+ return false;
+ }
+ input.advancePeekPosition(headerPosition);
+ } else {
+ if (++validFramesCount >= 4 && totalValidFramesSize > TsExtractor.TS_PACKET_SIZE) {
+ return true;
+ }
+
+ // Skip the frame.
+ input.peekFully(scratch.data, 0, 4);
+ scratchBits.setPosition(14);
+ int frameSize = scratchBits.readBits(13);
+ // Either the stream is malformed OR we're not parsing an ADTS stream.
+ if (frameSize <= 6) {
+ return false;
+ }
+ input.advancePeekPosition(frameSize - 6);
+ totalValidFramesSize += frameSize;
+ }
+ }
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.extractorOutput = output;
+ reader.createTracks(output, new TrackIdGenerator(0, 1));
+ output.endTracks();
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ startedPacket = false;
+ reader.seek();
+ firstSampleTimestampUs = timeUs;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ long inputLength = input.getLength();
+ boolean canUseConstantBitrateSeeking =
+ (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0 && inputLength != C.LENGTH_UNSET;
+ if (canUseConstantBitrateSeeking) {
+ calculateAverageFrameSize(input);
+ }
+
+ int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
+ boolean readEndOfStream = bytesRead == RESULT_END_OF_INPUT;
+ maybeOutputSeekMap(inputLength, canUseConstantBitrateSeeking, readEndOfStream);
+ if (readEndOfStream) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ // Feed whatever data we have to the reader, regardless of whether the read finished or not.
+ packetBuffer.setPosition(0);
+ packetBuffer.setLimit(bytesRead);
+
+ if (!startedPacket) {
+ // Pass data to the reader as though it's contained within a single infinitely long packet.
+ reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR);
+ startedPacket = true;
+ }
+ // TODO: Make it possible for reader to consume the dataSource directly, so that it becomes
+ // unnecessary to copy the data through packetBuffer.
+ reader.consume(packetBuffer);
+ return RESULT_CONTINUE;
+ }
+
+ private int peekId3Header(ExtractorInput input) throws IOException, InterruptedException {
+ int firstFramePosition = 0;
+ while (true) {
+ input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH);
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != ID3_TAG) {
+ break;
+ }
+ scratch.skipBytes(3);
+ int length = scratch.readSynchSafeInt();
+ firstFramePosition += ID3_HEADER_LENGTH + length;
+ input.advancePeekPosition(length);
+ }
+ input.resetPeekPosition();
+ input.advancePeekPosition(firstFramePosition);
+ if (this.firstFramePosition == C.POSITION_UNSET) {
+ this.firstFramePosition = firstFramePosition;
+ }
+ return firstFramePosition;
+ }
+
+ private void maybeOutputSeekMap(
+ long inputLength, boolean canUseConstantBitrateSeeking, boolean readEndOfStream) {
+ if (hasOutputSeekMap) {
+ return;
+ }
+ boolean useConstantBitrateSeeking = canUseConstantBitrateSeeking && averageFrameSize > 0;
+ if (useConstantBitrateSeeking
+ && reader.getSampleDurationUs() == C.TIME_UNSET
+ && !readEndOfStream) {
+ // Wait for the sampleDurationUs to be available, or for the end of the stream to be reached,
+ // before creating seek map.
+ return;
+ }
+
+ ExtractorOutput extractorOutput = Assertions.checkNotNull(this.extractorOutput);
+ if (useConstantBitrateSeeking && reader.getSampleDurationUs() != C.TIME_UNSET) {
+ extractorOutput.seekMap(getConstantBitrateSeekMap(inputLength));
+ } else {
+ extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ }
+ hasOutputSeekMap = true;
+ }
+
+ private void calculateAverageFrameSize(ExtractorInput input)
+ throws IOException, InterruptedException {
+ if (hasCalculatedAverageFrameSize) {
+ return;
+ }
+ averageFrameSize = C.LENGTH_UNSET;
+ input.resetPeekPosition();
+ if (input.getPosition() == 0) {
+ // Skip any ID3 headers.
+ peekId3Header(input);
+ }
+
+ int numValidFrames = 0;
+ long totalValidFramesSize = 0;
+ try {
+ while (input.peekFully(
+ scratch.data, /* offset= */ 0, /* length= */ 2, /* allowEndOfInput= */ true)) {
+ scratch.setPosition(0);
+ int syncBytes = scratch.readUnsignedShort();
+ if (!AdtsReader.isAdtsSyncWord(syncBytes)) {
+ // Invalid sync byte pattern.
+ // Constant bit-rate seeking will probably fail for this stream.
+ numValidFrames = 0;
+ break;
+ } else {
+ // Read the frame size.
+ if (!input.peekFully(
+ scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true)) {
+ break;
+ }
+ scratchBits.setPosition(14);
+ int currentFrameSize = scratchBits.readBits(13);
+ // Either the stream is malformed OR we're not parsing an ADTS stream.
+ if (currentFrameSize <= 6) {
+ hasCalculatedAverageFrameSize = true;
+ throw new ParserException("Malformed ADTS stream");
+ }
+ totalValidFramesSize += currentFrameSize;
+ if (++numValidFrames == NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE) {
+ break;
+ }
+ if (!input.advancePeekPosition(currentFrameSize - 6, /* allowEndOfInput= */ true)) {
+ break;
+ }
+ }
+ }
+ } catch (EOFException e) {
+ // We reached the end of the input during a peekFully() or advancePeekPosition() operation.
+ // This is OK, it just means the input has an incomplete ADTS frame at the end. Ideally
+ // ExtractorInput would allow these operations to encounter end-of-input without throwing an
+ // exception [internal: b/145586657].
+ }
+ input.resetPeekPosition();
+ if (numValidFrames > 0) {
+ averageFrameSize = (int) (totalValidFramesSize / numValidFrames);
+ } else {
+ averageFrameSize = C.LENGTH_UNSET;
+ }
+ hasCalculatedAverageFrameSize = true;
+ }
+
+ private SeekMap getConstantBitrateSeekMap(long inputLength) {
+ int bitrate = getBitrateFromFrameSize(averageFrameSize, reader.getSampleDurationUs());
+ return new ConstantBitrateSeekMap(inputLength, firstFramePosition, bitrate, averageFrameSize);
+ }
+
+ /**
+ * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds.
+ *
+ * @param frameSize The size of each frame in the stream.
+ * @param durationUsPerFrame The duration of the given frame in microseconds.
+ * @return The stream bitrate.
+ */
+ private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) {
+ return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
new file mode 100644
index 0000000000..f577747ec2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
@@ -0,0 +1,532 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Pair;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Parses a continuous ADTS byte stream and extracts individual frames.
+ */
+public final class AdtsReader implements ElementaryStreamReader {
+
+ private static final String TAG = "AdtsReader";
+
+ private static final int STATE_FINDING_SAMPLE = 0;
+ private static final int STATE_CHECKING_ADTS_HEADER = 1;
+ private static final int STATE_READING_ID3_HEADER = 2;
+ private static final int STATE_READING_ADTS_HEADER = 3;
+ private static final int STATE_READING_SAMPLE = 4;
+
+ private static final int HEADER_SIZE = 5;
+ private static final int CRC_SIZE = 2;
+
+ // Match states used while looking for the next sample
+ private static final int MATCH_STATE_VALUE_SHIFT = 8;
+ private static final int MATCH_STATE_START = 1 << MATCH_STATE_VALUE_SHIFT;
+ private static final int MATCH_STATE_FF = 2 << MATCH_STATE_VALUE_SHIFT;
+ private static final int MATCH_STATE_I = 3 << MATCH_STATE_VALUE_SHIFT;
+ private static final int MATCH_STATE_ID = 4 << MATCH_STATE_VALUE_SHIFT;
+
+ private static final int ID3_HEADER_SIZE = 10;
+ private static final int ID3_SIZE_OFFSET = 6;
+ private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'};
+ private static final int VERSION_UNSET = -1;
+
+ private final boolean exposeId3;
+ private final ParsableBitArray adtsScratch;
+ private final ParsableByteArray id3HeaderBuffer;
+ private final String language;
+
+ private String formatId;
+ private TrackOutput output;
+ private TrackOutput id3Output;
+
+ private int state;
+ private int bytesRead;
+
+ private int matchState;
+
+ private boolean hasCrc;
+ private boolean foundFirstFrame;
+
+ // Used to verifies sync words
+ private int firstFrameVersion;
+ private int firstFrameSampleRateIndex;
+
+ private int currentFrameVersion;
+
+ // Used when parsing the header.
+ private boolean hasOutputFormat;
+ private long sampleDurationUs;
+ private int sampleSize;
+
+ // Used when reading the samples.
+ private long timeUs;
+
+ private TrackOutput currentOutput;
+ private long currentSampleDuration;
+
+ /**
+ * @param exposeId3 True if the reader should expose ID3 information.
+ */
+ public AdtsReader(boolean exposeId3) {
+ this(exposeId3, null);
+ }
+
+ /**
+ * @param exposeId3 True if the reader should expose ID3 information.
+ * @param language Track language.
+ */
+ public AdtsReader(boolean exposeId3, String language) {
+ adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
+ id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE));
+ setFindingSampleState();
+ firstFrameVersion = VERSION_UNSET;
+ firstFrameSampleRateIndex = C.INDEX_UNSET;
+ sampleDurationUs = C.TIME_UNSET;
+ this.exposeId3 = exposeId3;
+ this.language = language;
+ }
+
+ /** Returns whether an integer matches an ADTS SYNC word. */
+ public static boolean isAdtsSyncWord(int candidateSyncWord) {
+ return (candidateSyncWord & 0xFFF6) == 0xFFF0;
+ }
+
+ @Override
+ public void seek() {
+ resetSync();
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
+ if (exposeId3) {
+ idGenerator.generateNewId();
+ id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
+ id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(),
+ MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null));
+ } else {
+ id3Output = new DummyTrackOutput();
+ }
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ timeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) throws ParserException {
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_SAMPLE:
+ findNextSample(data);
+ break;
+ case STATE_READING_ID3_HEADER:
+ if (continueRead(data, id3HeaderBuffer.data, ID3_HEADER_SIZE)) {
+ parseId3Header();
+ }
+ break;
+ case STATE_CHECKING_ADTS_HEADER:
+ checkAdtsHeader(data);
+ break;
+ case STATE_READING_ADTS_HEADER:
+ int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
+ if (continueRead(data, adtsScratch.data, targetLength)) {
+ parseAdtsHeader();
+ }
+ break;
+ case STATE_READING_SAMPLE:
+ readSample(data);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Returns the duration in microseconds per sample, or {@link C#TIME_UNSET} if the sample duration
+ * is not available.
+ */
+ public long getSampleDurationUs() {
+ return sampleDurationUs;
+ }
+
+ private void resetSync() {
+ foundFirstFrame = false;
+ setFindingSampleState();
+ }
+
+ /**
+ * Continues a read from the provided {@code source} into a given {@code target}. It's assumed
+ * that the data should be written into {@code target} starting from an offset of zero.
+ *
+ * @param source The source from which to read.
+ * @param target The target into which data is to be read.
+ * @param targetLength The target length of the read.
+ * @return Whether the target length was reached.
+ */
+ private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
+ int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
+ source.readBytes(target, bytesRead, bytesToRead);
+ bytesRead += bytesToRead;
+ return bytesRead == targetLength;
+ }
+
+ /**
+ * Sets the state to STATE_FINDING_SAMPLE.
+ */
+ private void setFindingSampleState() {
+ state = STATE_FINDING_SAMPLE;
+ bytesRead = 0;
+ matchState = MATCH_STATE_START;
+ }
+
+ /**
+ * Sets the state to STATE_READING_ID3_HEADER and resets the fields required for
+ * {@link #parseId3Header()}.
+ */
+ private void setReadingId3HeaderState() {
+ state = STATE_READING_ID3_HEADER;
+ bytesRead = ID3_IDENTIFIER.length;
+ sampleSize = 0;
+ id3HeaderBuffer.setPosition(0);
+ }
+
+ /**
+ * Sets the state to STATE_READING_SAMPLE.
+ *
+ * @param outputToUse TrackOutput object to write the sample to
+ * @param currentSampleDuration Duration of the sample to be read
+ * @param priorReadBytes Size of prior read bytes
+ * @param sampleSize Size of the sample
+ */
+ private void setReadingSampleState(TrackOutput outputToUse, long currentSampleDuration,
+ int priorReadBytes, int sampleSize) {
+ state = STATE_READING_SAMPLE;
+ bytesRead = priorReadBytes;
+ this.currentOutput = outputToUse;
+ this.currentSampleDuration = currentSampleDuration;
+ this.sampleSize = sampleSize;
+ }
+
+ /**
+ * Sets the state to STATE_READING_ADTS_HEADER.
+ */
+ private void setReadingAdtsHeaderState() {
+ state = STATE_READING_ADTS_HEADER;
+ bytesRead = 0;
+ }
+
+ /** Sets the state to STATE_CHECKING_ADTS_HEADER. */
+ private void setCheckingAdtsHeaderState() {
+ state = STATE_CHECKING_ADTS_HEADER;
+ bytesRead = 0;
+ }
+
+ /**
+ * Locates the next sample start, advancing the position to the byte that immediately follows
+ * identifier. If a sample was not located, the position is advanced to the limit.
+ *
+ * @param pesBuffer The buffer whose position should be advanced.
+ */
+ private void findNextSample(ParsableByteArray pesBuffer) {
+ byte[] adtsData = pesBuffer.data;
+ int position = pesBuffer.getPosition();
+ int endOffset = pesBuffer.limit();
+ while (position < endOffset) {
+ int data = adtsData[position++] & 0xFF;
+ if (matchState == MATCH_STATE_FF && isAdtsSyncBytes((byte) 0xFF, (byte) data)) {
+ if (foundFirstFrame
+ || checkSyncPositionValid(pesBuffer, /* syncPositionCandidate= */ position - 2)) {
+ currentFrameVersion = (data & 0x8) >> 3;
+ hasCrc = (data & 0x1) == 0;
+ if (!foundFirstFrame) {
+ setCheckingAdtsHeaderState();
+ } else {
+ setReadingAdtsHeaderState();
+ }
+ pesBuffer.setPosition(position);
+ return;
+ }
+ }
+
+ switch (matchState | data) {
+ case MATCH_STATE_START | 0xFF:
+ matchState = MATCH_STATE_FF;
+ break;
+ case MATCH_STATE_START | 'I':
+ matchState = MATCH_STATE_I;
+ break;
+ case MATCH_STATE_I | 'D':
+ matchState = MATCH_STATE_ID;
+ break;
+ case MATCH_STATE_ID | '3':
+ setReadingId3HeaderState();
+ pesBuffer.setPosition(position);
+ return;
+ default:
+ if (matchState != MATCH_STATE_START) {
+ // If matching fails in a later state, revert to MATCH_STATE_START and
+ // check this byte again
+ matchState = MATCH_STATE_START;
+ position--;
+ }
+ break;
+ }
+ }
+ pesBuffer.setPosition(position);
+ }
+
+ /**
+ * Peeks the Adts header of the current frame and checks if it is valid. If the header is valid,
+ * transition to {@link #STATE_READING_ADTS_HEADER}; else, transition to {@link
+ * #STATE_FINDING_SAMPLE}.
+ */
+ private void checkAdtsHeader(ParsableByteArray buffer) {
+ if (buffer.bytesLeft() == 0) {
+ // Not enough data to check yet, defer this check.
+ return;
+ }
+ // Peek the next byte of buffer into scratch array.
+ adtsScratch.data[0] = buffer.data[buffer.getPosition()];
+
+ adtsScratch.setPosition(2);
+ int currentFrameSampleRateIndex = adtsScratch.readBits(4);
+ if (firstFrameSampleRateIndex != C.INDEX_UNSET
+ && currentFrameSampleRateIndex != firstFrameSampleRateIndex) {
+ // Invalid header.
+ resetSync();
+ return;
+ }
+
+ if (!foundFirstFrame) {
+ foundFirstFrame = true;
+ firstFrameVersion = currentFrameVersion;
+ firstFrameSampleRateIndex = currentFrameSampleRateIndex;
+ }
+ setReadingAdtsHeaderState();
+ }
+
+ /**
+ * Checks whether a candidate SYNC word position is likely to be the position of a real SYNC word.
+ * The caller must check that the first byte of the SYNC word is 0xFF before calling this method.
+ * This method performs the following checks:
+ *
+ * <ul>
+ * <li>The MPEG version of this frame must match the previously detected version.
+ * <li>The sample rate index of this frame must match the previously detected sample rate index.
+ * <li>The frame size must be at least 7 bytes
+ * <li>The bytes following the frame must be either another SYNC word with the same MPEG
+ * version, or the start of an ID3 header.
+ * </ul>
+ *
+ * With the exception of the first check, if there is insufficient data in the buffer then checks
+ * are optimistically skipped and {@code true} is returned.
+ *
+ * @param pesBuffer The buffer containing at data to check.
+ * @param syncPositionCandidate The candidate SYNC word position. May be -1 if the first byte of
+ * the candidate was the last byte of the previously consumed buffer.
+ * @return True if all checks were passed or skipped, indicating the position is likely to be the
+ * position of a real SYNC word. False otherwise.
+ */
+ private boolean checkSyncPositionValid(ParsableByteArray pesBuffer, int syncPositionCandidate) {
+ pesBuffer.setPosition(syncPositionCandidate + 1);
+ if (!tryRead(pesBuffer, adtsScratch.data, 1)) {
+ return false;
+ }
+
+ // The MPEG version of this frame must match the previously detected version.
+ adtsScratch.setPosition(4);
+ int currentFrameVersion = adtsScratch.readBits(1);
+ if (firstFrameVersion != VERSION_UNSET && currentFrameVersion != firstFrameVersion) {
+ return false;
+ }
+
+ // The sample rate index of this frame must match the previously detected sample rate index.
+ if (firstFrameSampleRateIndex != C.INDEX_UNSET) {
+ if (!tryRead(pesBuffer, adtsScratch.data, 1)) {
+ // Insufficient data for further checks.
+ return true;
+ }
+ adtsScratch.setPosition(2);
+ int currentFrameSampleRateIndex = adtsScratch.readBits(4);
+ if (currentFrameSampleRateIndex != firstFrameSampleRateIndex) {
+ return false;
+ }
+ pesBuffer.setPosition(syncPositionCandidate + 2);
+ }
+
+ // The frame size must be at least 7 bytes.
+ if (!tryRead(pesBuffer, adtsScratch.data, 4)) {
+ // Insufficient data for further checks.
+ return true;
+ }
+ adtsScratch.setPosition(14);
+ int frameSize = adtsScratch.readBits(13);
+ if (frameSize < 7) {
+ return false;
+ }
+
+ // The bytes following the frame must be either another SYNC word with the same MPEG version, or
+ // the start of an ID3 header.
+ byte[] data = pesBuffer.data;
+ int dataLimit = pesBuffer.limit();
+ int nextSyncPosition = syncPositionCandidate + frameSize;
+ if (nextSyncPosition >= dataLimit) {
+ // Insufficient data for further checks.
+ return true;
+ }
+ if (data[nextSyncPosition] == (byte) 0xFF) {
+ if (nextSyncPosition + 1 == dataLimit) {
+ // Insufficient data for further checks.
+ return true;
+ }
+ return isAdtsSyncBytes((byte) 0xFF, data[nextSyncPosition + 1])
+ && ((data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion;
+ } else {
+ if (data[nextSyncPosition] != 'I') {
+ return false;
+ }
+ if (nextSyncPosition + 1 == dataLimit) {
+ // Insufficient data for further checks.
+ return true;
+ }
+ if (data[nextSyncPosition + 1] != 'D') {
+ return false;
+ }
+ if (nextSyncPosition + 2 == dataLimit) {
+ // Insufficient data for further checks.
+ return true;
+ }
+ return data[nextSyncPosition + 2] == '3';
+ }
+ }
+
+ private boolean isAdtsSyncBytes(byte firstByte, byte secondByte) {
+ int syncWord = (firstByte & 0xFF) << 8 | (secondByte & 0xFF);
+ return isAdtsSyncWord(syncWord);
+ }
+
+ /** Reads {@code targetLength} bytes into target, and returns whether the read succeeded. */
+ private boolean tryRead(ParsableByteArray source, byte[] target, int targetLength) {
+ if (source.bytesLeft() < targetLength) {
+ return false;
+ }
+ source.readBytes(target, /* offset= */ 0, targetLength);
+ return true;
+ }
+
+ /**
+ * Parses the Id3 header.
+ */
+ private void parseId3Header() {
+ id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE);
+ id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET);
+ setReadingSampleState(id3Output, 0, ID3_HEADER_SIZE,
+ id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE);
+ }
+
+ /**
+ * Parses the sample header.
+ */
+ private void parseAdtsHeader() throws ParserException {
+ adtsScratch.setPosition(0);
+
+ if (!hasOutputFormat) {
+ int audioObjectType = adtsScratch.readBits(2) + 1;
+ if (audioObjectType != 2) {
+ // The stream indicates AAC-Main (1), AAC-SSR (3) or AAC-LTP (4). When the stream indicates
+ // AAC-Main it's more likely that the stream contains HE-AAC (5), which cannot be
+ // represented correctly in the 2 bit audio_object_type field in the ADTS header. In
+ // practice when the stream indicates AAC-SSR or AAC-LTP it more commonly contains AAC-LC or
+ // HE-AAC. Since most Android devices don't support AAC-Main, AAC-SSR or AAC-LTP, and since
+ // indicating AAC-LC works for HE-AAC streams, we pretend that we're dealing with AAC-LC and
+ // hope for the best. In practice this often works.
+ // See: https://github.com/google/ExoPlayer/issues/774
+ // See: https://github.com/google/ExoPlayer/issues/1383
+ Log.w(TAG, "Detected audio object type: " + audioObjectType + ", but assuming AAC LC.");
+ audioObjectType = 2;
+ }
+
+ adtsScratch.skipBits(5);
+ int channelConfig = adtsScratch.readBits(3);
+
+ byte[] audioSpecificConfig =
+ CodecSpecificDataUtil.buildAacAudioSpecificConfig(
+ audioObjectType, firstFrameSampleRateIndex, channelConfig);
+ Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig(
+ audioSpecificConfig);
+
+ Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null,
+ Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first,
+ Collections.singletonList(audioSpecificConfig), null, 0, language);
+ // In this class a sample is an access unit, but the MediaFormat sample rate specifies the
+ // number of PCM audio samples per second.
+ sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate;
+ output.format(format);
+ hasOutputFormat = true;
+ } else {
+ adtsScratch.skipBits(10);
+ }
+
+ adtsScratch.skipBits(4);
+ int sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE;
+ if (hasCrc) {
+ sampleSize -= CRC_SIZE;
+ }
+
+ setReadingSampleState(output, sampleDurationUs, 0, sampleSize);
+ }
+
+ /**
+ * Reads the rest of the sample
+ */
+ private void readSample(ParsableByteArray data) {
+ int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
+ currentOutput.sampleData(data, bytesToRead);
+ bytesRead += bytesToRead;
+ if (bytesRead == sampleSize) {
+ currentOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ timeUs += currentSampleDuration;
+ setFindingSampleState();
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
new file mode 100644
index 0000000000..cfbc64d2ee
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
@@ -0,0 +1,283 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import android.util.SparseArray;
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.Cea708InitializationData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Default {@link TsPayloadReader.Factory} implementation.
+ */
+public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory {
+
+ /**
+ * Flags controlling elementary stream readers' behavior. Possible flag values are {@link
+ * #FLAG_ALLOW_NON_IDR_KEYFRAMES}, {@link #FLAG_IGNORE_AAC_STREAM}, {@link
+ * #FLAG_IGNORE_H264_STREAM}, {@link #FLAG_DETECT_ACCESS_UNITS}, {@link
+ * #FLAG_IGNORE_SPLICE_INFO_STREAM}, {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} and {@link
+ * #FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ FLAG_ALLOW_NON_IDR_KEYFRAMES,
+ FLAG_IGNORE_AAC_STREAM,
+ FLAG_IGNORE_H264_STREAM,
+ FLAG_DETECT_ACCESS_UNITS,
+ FLAG_IGNORE_SPLICE_INFO_STREAM,
+ FLAG_OVERRIDE_CAPTION_DESCRIPTORS,
+ FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS
+ })
+ public @interface Flags {}
+
+ /**
+ * When extracting H.264 samples, whether to treat samples consisting of non-IDR I slices as
+ * synchronization samples (key-frames).
+ */
+ public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1;
+ /**
+ * Prevents the creation of {@link AdtsReader} and {@link LatmReader} instances. This flag should
+ * be enabled if the transport stream contains no packets for an AAC elementary stream that is
+ * declared in the PMT.
+ */
+ public static final int FLAG_IGNORE_AAC_STREAM = 1 << 1;
+ /**
+ * Prevents the creation of {@link H264Reader} instances. This flag should be enabled if the
+ * transport stream contains no packets for an H.264 elementary stream that is declared in the
+ * PMT.
+ */
+ public static final int FLAG_IGNORE_H264_STREAM = 1 << 2;
+ /**
+ * When extracting H.264 samples, whether to split the input stream into access units (samples)
+ * based on slice headers. This flag should be disabled if the stream contains access unit
+ * delimiters (AUDs).
+ */
+ public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3;
+ /** Prevents the creation of {@link SpliceInfoSectionReader} instances. */
+ public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4;
+ /**
+ * Whether the list of {@code closedCaptionFormats} passed to {@link
+ * DefaultTsPayloadReaderFactory#DefaultTsPayloadReaderFactory(int, List)} should be used in spite
+ * of any closed captions service descriptors. If this flag is disabled, {@code
+ * closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors.
+ */
+ public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5;
+ /**
+ * Sets whether HDMV DTS audio streams will be handled. If this flag is set, SCTE subtitles will
+ * not be detected, as they share the same elementary stream type as HDMV DTS.
+ */
+ public static final int FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS = 1 << 6;
+
+ private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86;
+
+ @Flags private final int flags;
+ private final List<Format> closedCaptionFormats;
+
+ public DefaultTsPayloadReaderFactory() {
+ this(0);
+ }
+
+ /**
+ * @param flags A combination of {@code FLAG_*} values that control the behavior of the created
+ * readers.
+ */
+ public DefaultTsPayloadReaderFactory(@Flags int flags) {
+ this(
+ flags,
+ Collections.singletonList(
+ Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)));
+ }
+
+ /**
+ * @param flags A combination of {@code FLAG_*} values that control the behavior of the created
+ * readers.
+ * @param closedCaptionFormats {@link Format}s to be exposed by payload readers for streams with
+ * embedded closed captions when no caption service descriptors are provided. If
+ * {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, {@code closedCaptionFormats} overrides
+ * any descriptor information. If not set, and {@code closedCaptionFormats} is empty, a
+ * closed caption track with {@link Format#accessibilityChannel} {@link Format#NO_VALUE} will
+ * be exposed.
+ */
+ public DefaultTsPayloadReaderFactory(@Flags int flags, List<Format> closedCaptionFormats) {
+ this.flags = flags;
+ this.closedCaptionFormats = closedCaptionFormats;
+ }
+
+ @Override
+ public SparseArray<TsPayloadReader> createInitialPayloadReaders() {
+ return new SparseArray<>();
+ }
+
+ @Override
+ @SuppressWarnings("fallthrough")
+ public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) {
+ switch (streamType) {
+ case TsExtractor.TS_STREAM_TYPE_MPA:
+ case TsExtractor.TS_STREAM_TYPE_MPA_LSF:
+ return new PesReader(new MpegAudioReader(esInfo.language));
+ case TsExtractor.TS_STREAM_TYPE_AAC_ADTS:
+ return isSet(FLAG_IGNORE_AAC_STREAM)
+ ? null : new PesReader(new AdtsReader(false, esInfo.language));
+ case TsExtractor.TS_STREAM_TYPE_AAC_LATM:
+ return isSet(FLAG_IGNORE_AAC_STREAM)
+ ? null : new PesReader(new LatmReader(esInfo.language));
+ case TsExtractor.TS_STREAM_TYPE_AC3:
+ case TsExtractor.TS_STREAM_TYPE_E_AC3:
+ return new PesReader(new Ac3Reader(esInfo.language));
+ case TsExtractor.TS_STREAM_TYPE_AC4:
+ return new PesReader(new Ac4Reader(esInfo.language));
+ case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
+ if (!isSet(FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS)) {
+ return null;
+ }
+ // Fall through.
+ case TsExtractor.TS_STREAM_TYPE_DTS:
+ return new PesReader(new DtsReader(esInfo.language));
+ case TsExtractor.TS_STREAM_TYPE_H262:
+ return new PesReader(new H262Reader(buildUserDataReader(esInfo)));
+ case TsExtractor.TS_STREAM_TYPE_H264:
+ return isSet(FLAG_IGNORE_H264_STREAM) ? null
+ : new PesReader(new H264Reader(buildSeiReader(esInfo),
+ isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS)));
+ case TsExtractor.TS_STREAM_TYPE_H265:
+ return new PesReader(new H265Reader(buildSeiReader(esInfo)));
+ case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO:
+ return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM)
+ ? null : new SectionReader(new SpliceInfoSectionReader());
+ case TsExtractor.TS_STREAM_TYPE_ID3:
+ return new PesReader(new Id3Reader());
+ case TsExtractor.TS_STREAM_TYPE_DVBSUBS:
+ return new PesReader(
+ new DvbSubtitleReader(esInfo.dvbSubtitleInfos));
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for
+ * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a
+ * {@link SeiReader} for the declared formats, or {@link #closedCaptionFormats} if the descriptor
+ * is not present.
+ *
+ * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}.
+ * @return A {@link SeiReader} for closed caption tracks.
+ */
+ private SeiReader buildSeiReader(EsInfo esInfo) {
+ return new SeiReader(getClosedCaptionFormats(esInfo));
+ }
+
+ /**
+ * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link UserDataReader} for
+ * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a
+ * {@link UserDataReader} for the declared formats, or {@link #closedCaptionFormats} if the
+ * descriptor is not present.
+ *
+ * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}.
+ * @return A {@link UserDataReader} for closed caption tracks.
+ */
+ private UserDataReader buildUserDataReader(EsInfo esInfo) {
+ return new UserDataReader(getClosedCaptionFormats(esInfo));
+ }
+
+ /**
+ * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link List<Format>} of {@link
+ * #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a {@link
+ * List<Format>} for the declared formats, or {@link #closedCaptionFormats} if the descriptor is
+ * not present.
+ *
+ * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}.
+ * @return A {@link List<Format>} containing list of closed caption formats.
+ */
+ private List<Format> getClosedCaptionFormats(EsInfo esInfo) {
+ if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) {
+ return closedCaptionFormats;
+ }
+ ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes);
+ List<Format> closedCaptionFormats = this.closedCaptionFormats;
+ while (scratchDescriptorData.bytesLeft() > 0) {
+ int descriptorTag = scratchDescriptorData.readUnsignedByte();
+ int descriptorLength = scratchDescriptorData.readUnsignedByte();
+ int nextDescriptorPosition = scratchDescriptorData.getPosition() + descriptorLength;
+ if (descriptorTag == DESCRIPTOR_TAG_CAPTION_SERVICE) {
+ // Note: see ATSC A/65 for detailed information about the caption service descriptor.
+ closedCaptionFormats = new ArrayList<>();
+ int numberOfServices = scratchDescriptorData.readUnsignedByte() & 0x1F;
+ for (int i = 0; i < numberOfServices; i++) {
+ String language = scratchDescriptorData.readString(3);
+ int captionTypeByte = scratchDescriptorData.readUnsignedByte();
+ boolean isDigital = (captionTypeByte & 0x80) != 0;
+ String mimeType;
+ int accessibilityChannel;
+ if (isDigital) {
+ mimeType = MimeTypes.APPLICATION_CEA708;
+ accessibilityChannel = captionTypeByte & 0x3F;
+ } else {
+ mimeType = MimeTypes.APPLICATION_CEA608;
+ accessibilityChannel = 1;
+ }
+
+ // easy_reader(1), wide_aspect_ratio(1), reserved(6).
+ byte flags = (byte) scratchDescriptorData.readUnsignedByte();
+ // Skip reserved (8).
+ scratchDescriptorData.skipBytes(1);
+
+ List<byte[]> initializationData = null;
+ // The wide_aspect_ratio flag only has meaning for CEA-708.
+ if (isDigital) {
+ boolean isWideAspectRatio = (flags & 0x40) != 0;
+ initializationData = Cea708InitializationData.buildData(isWideAspectRatio);
+ }
+
+ closedCaptionFormats.add(
+ Format.createTextSampleFormat(
+ /* id= */ null,
+ mimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ /* selectionFlags= */ 0,
+ language,
+ accessibilityChannel,
+ /* drmInitData= */ null,
+ Format.OFFSET_SAMPLE_RELATIVE,
+ initializationData));
+ }
+ } else {
+ // Unknown descriptor. Ignore.
+ }
+ scratchDescriptorData.setPosition(nextDescriptorPosition);
+ }
+
+ return closedCaptionFormats;
+ }
+
+ private boolean isSet(@Flags int flag) {
+ return (flags & flag) != 0;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java
new file mode 100644
index 0000000000..a4205add7b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java
@@ -0,0 +1,181 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.DtsUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Parses a continuous DTS byte stream and extracts individual samples.
+ */
+public final class DtsReader implements ElementaryStreamReader {
+
+ private static final int STATE_FINDING_SYNC = 0;
+ private static final int STATE_READING_HEADER = 1;
+ private static final int STATE_READING_SAMPLE = 2;
+
+ private static final int HEADER_SIZE = 18;
+
+ private final ParsableByteArray headerScratchBytes;
+ private final String language;
+
+ private String formatId;
+ private TrackOutput output;
+
+ private int state;
+ private int bytesRead;
+
+ // Used to find the header.
+ private int syncBytes;
+
+ // Used when parsing the header.
+ private long sampleDurationUs;
+ private Format format;
+ private int sampleSize;
+
+ // Used when reading the samples.
+ private long timeUs;
+
+ /**
+ * Constructs a new reader for DTS elementary streams.
+ *
+ * @param language Track language.
+ */
+ public DtsReader(String language) {
+ headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]);
+ state = STATE_FINDING_SYNC;
+ this.language = language;
+ }
+
+ @Override
+ public void seek() {
+ state = STATE_FINDING_SYNC;
+ bytesRead = 0;
+ syncBytes = 0;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ timeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_SYNC:
+ if (skipToNextSync(data)) {
+ state = STATE_READING_HEADER;
+ }
+ break;
+ case STATE_READING_HEADER:
+ if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) {
+ parseHeader();
+ headerScratchBytes.setPosition(0);
+ output.sampleData(headerScratchBytes, HEADER_SIZE);
+ state = STATE_READING_SAMPLE;
+ }
+ break;
+ case STATE_READING_SAMPLE:
+ int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
+ output.sampleData(data, bytesToRead);
+ bytesRead += bytesToRead;
+ if (bytesRead == sampleSize) {
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ timeUs += sampleDurationUs;
+ state = STATE_FINDING_SYNC;
+ }
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Continues a read from the provided {@code source} into a given {@code target}. It's assumed
+ * that the data should be written into {@code target} starting from an offset of zero.
+ *
+ * @param source The source from which to read.
+ * @param target The target into which data is to be read.
+ * @param targetLength The target length of the read.
+ * @return Whether the target length was reached.
+ */
+ private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
+ int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
+ source.readBytes(target, bytesRead, bytesToRead);
+ bytesRead += bytesToRead;
+ return bytesRead == targetLength;
+ }
+
+ /**
+ * Locates the next SYNC value in the buffer, advancing the position to the byte that immediately
+ * follows it. If SYNC was not located, the position is advanced to the limit.
+ *
+ * @param pesBuffer The buffer whose position should be advanced.
+ * @return Whether SYNC was found.
+ */
+ private boolean skipToNextSync(ParsableByteArray pesBuffer) {
+ while (pesBuffer.bytesLeft() > 0) {
+ syncBytes <<= 8;
+ syncBytes |= pesBuffer.readUnsignedByte();
+ if (DtsUtil.isSyncWord(syncBytes)) {
+ headerScratchBytes.data[0] = (byte) ((syncBytes >> 24) & 0xFF);
+ headerScratchBytes.data[1] = (byte) ((syncBytes >> 16) & 0xFF);
+ headerScratchBytes.data[2] = (byte) ((syncBytes >> 8) & 0xFF);
+ headerScratchBytes.data[3] = (byte) (syncBytes & 0xFF);
+ bytesRead = 4;
+ syncBytes = 0;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Parses the sample header.
+ */
+ private void parseHeader() {
+ byte[] frameData = headerScratchBytes.data;
+ if (format == null) {
+ format = DtsUtil.parseDtsFormat(frameData, formatId, language, null);
+ output.format(format);
+ }
+ sampleSize = DtsUtil.getDtsFrameSize(frameData);
+ // In this class a sample is an access unit (frame in DTS), but the format's sample rate
+ // specifies the number of PCM audio samples per second.
+ sampleDurationUs = (int) (C.MICROS_PER_SECOND
+ * DtsUtil.parseDtsAudioSampleCount(frameData) / format.sampleRate);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java
new file mode 100644
index 0000000000..aceab78bf0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parses DVB subtitle data and extracts individual frames.
+ */
+public final class DvbSubtitleReader implements ElementaryStreamReader {
+
+ private final List<DvbSubtitleInfo> subtitleInfos;
+ private final TrackOutput[] outputs;
+
+ private boolean writingSample;
+ private int bytesToCheck;
+ private int sampleBytesWritten;
+ private long sampleTimeUs;
+
+ /**
+ * @param subtitleInfos Information about the DVB subtitles associated to the stream.
+ */
+ public DvbSubtitleReader(List<DvbSubtitleInfo> subtitleInfos) {
+ this.subtitleInfos = subtitleInfos;
+ outputs = new TrackOutput[subtitleInfos.size()];
+ }
+
+ @Override
+ public void seek() {
+ writingSample = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ for (int i = 0; i < outputs.length; i++) {
+ DvbSubtitleInfo subtitleInfo = subtitleInfos.get(i);
+ idGenerator.generateNewId();
+ TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
+ output.format(
+ Format.createImageSampleFormat(
+ idGenerator.getFormatId(),
+ MimeTypes.APPLICATION_DVBSUBS,
+ null,
+ Format.NO_VALUE,
+ 0,
+ Collections.singletonList(subtitleInfo.initializationData),
+ subtitleInfo.language,
+ null));
+ outputs[i] = output;
+ }
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
+ return;
+ }
+ writingSample = true;
+ sampleTimeUs = pesTimeUs;
+ sampleBytesWritten = 0;
+ bytesToCheck = 2;
+ }
+
+ @Override
+ public void packetFinished() {
+ if (writingSample) {
+ for (TrackOutput output : outputs) {
+ output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null);
+ }
+ writingSample = false;
+ }
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ if (writingSample) {
+ if (bytesToCheck == 2 && !checkNextByte(data, 0x20)) {
+ // Failed to check data_identifier
+ return;
+ }
+ if (bytesToCheck == 1 && !checkNextByte(data, 0x00)) {
+ // Check and discard the subtitle_stream_id
+ return;
+ }
+ int dataPosition = data.getPosition();
+ int bytesAvailable = data.bytesLeft();
+ for (TrackOutput output : outputs) {
+ data.setPosition(dataPosition);
+ output.sampleData(data, bytesAvailable);
+ }
+ sampleBytesWritten += bytesAvailable;
+ }
+ }
+
+ private boolean checkNextByte(ParsableByteArray data, int expectedValue) {
+ if (data.bytesLeft() == 0) {
+ return false;
+ }
+ if (data.readUnsignedByte() != expectedValue) {
+ writingSample = false;
+ }
+ bytesToCheck--;
+ return writingSample;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
new file mode 100644
index 0000000000..edd33d02c2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
@@ -0,0 +1,63 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Extracts individual samples from an elementary media stream, preserving original order.
+ */
+public interface ElementaryStreamReader {
+
+ /**
+ * Notifies the reader that a seek has occurred.
+ */
+ void seek();
+
+ /**
+ * Initializes the reader by providing outputs and ids for the tracks.
+ *
+ * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.
+ * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the
+ * {@link TrackOutput}s.
+ */
+ void createTracks(ExtractorOutput extractorOutput, PesReader.TrackIdGenerator idGenerator);
+
+ /**
+ * Called when a packet starts.
+ *
+ * @param pesTimeUs The timestamp associated with the packet.
+ * @param flags See {@link TsPayloadReader.Flags}.
+ */
+ void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags);
+
+ /**
+ * Consumes (possibly partial) data from the current packet.
+ *
+ * @param data The data to consume.
+ * @throws ParserException If the data could not be parsed.
+ */
+ void consume(ParsableByteArray data) throws ParserException;
+
+ /**
+ * Called when a packet ends.
+ */
+ void packetFinished();
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java
new file mode 100644
index 0000000000..576607366e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java
@@ -0,0 +1,333 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Pair;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Parses a continuous H262 byte stream and extracts individual frames.
+ */
+public final class H262Reader implements ElementaryStreamReader {
+
+ private static final int START_PICTURE = 0x00;
+ private static final int START_SEQUENCE_HEADER = 0xB3;
+ private static final int START_EXTENSION = 0xB5;
+ private static final int START_GROUP = 0xB8;
+ private static final int START_USER_DATA = 0xB2;
+
+ private String formatId;
+ private TrackOutput output;
+
+ // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4.
+ private static final double[] FRAME_RATE_VALUES = new double[] {
+ 24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60};
+
+ // State that should not be reset on seek.
+ private boolean hasOutputFormat;
+ private long frameDurationUs;
+
+ private final UserDataReader userDataReader;
+ private final ParsableByteArray userDataParsable;
+
+ // State that should be reset on seek.
+ private final boolean[] prefixFlags;
+ private final CsdBuffer csdBuffer;
+ private final NalUnitTargetBuffer userData;
+ private long totalBytesWritten;
+ private boolean startedFirstSample;
+
+ // Per packet state that gets reset at the start of each packet.
+ private long pesTimeUs;
+
+ // Per sample state that gets reset at the start of each sample.
+ private long samplePosition;
+ private long sampleTimeUs;
+ private boolean sampleIsKeyframe;
+ private boolean sampleHasPicture;
+
+ public H262Reader() {
+ this(null);
+ }
+
+ /* package */ H262Reader(UserDataReader userDataReader) {
+ this.userDataReader = userDataReader;
+ prefixFlags = new boolean[4];
+ csdBuffer = new CsdBuffer(128);
+ if (userDataReader != null) {
+ userData = new NalUnitTargetBuffer(START_USER_DATA, 128);
+ userDataParsable = new ParsableByteArray();
+ } else {
+ userData = null;
+ userDataParsable = null;
+ }
+ }
+
+ @Override
+ public void seek() {
+ NalUnitUtil.clearPrefixFlags(prefixFlags);
+ csdBuffer.reset();
+ if (userDataReader != null) {
+ userData.reset();
+ }
+ totalBytesWritten = 0;
+ startedFirstSample = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
+ if (userDataReader != null) {
+ userDataReader.createTracks(extractorOutput, idGenerator);
+ }
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ // TODO (Internal b/32267012): Consider using random access indicator.
+ this.pesTimeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ int offset = data.getPosition();
+ int limit = data.limit();
+ byte[] dataArray = data.data;
+
+ // Append the data to the buffer.
+ totalBytesWritten += data.bytesLeft();
+ output.sampleData(data, data.bytesLeft());
+
+ while (true) {
+ int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);
+
+ if (startCodeOffset == limit) {
+ // We've scanned to the end of the data without finding another start code.
+ if (!hasOutputFormat) {
+ csdBuffer.onData(dataArray, offset, limit);
+ }
+ if (userDataReader != null) {
+ userData.appendToNalUnit(dataArray, offset, limit);
+ }
+ return;
+ }
+
+ // We've found a start code with the following value.
+ int startCodeValue = data.data[startCodeOffset + 3] & 0xFF;
+ // This is the number of bytes from the current offset to the start of the next start
+ // code. It may be negative if the start code started in the previously consumed data.
+ int lengthToStartCode = startCodeOffset - offset;
+
+ if (!hasOutputFormat) {
+ if (lengthToStartCode > 0) {
+ csdBuffer.onData(dataArray, offset, startCodeOffset);
+ }
+ // This is the number of bytes belonging to the next start code that have already been
+ // passed to csdBuffer.
+ int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0;
+ if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) {
+ // The csd data is complete, so we can decode and output the media format.
+ Pair<Format, Long> result = parseCsdBuffer(csdBuffer, formatId);
+ output.format(result.first);
+ frameDurationUs = result.second;
+ hasOutputFormat = true;
+ }
+ }
+ if (userDataReader != null) {
+ int bytesAlreadyPassed = 0;
+ if (lengthToStartCode > 0) {
+ userData.appendToNalUnit(dataArray, offset, startCodeOffset);
+ } else {
+ bytesAlreadyPassed = -lengthToStartCode;
+ }
+
+ if (userData.endNalUnit(bytesAlreadyPassed)) {
+ int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength);
+ userDataParsable.reset(userData.nalData, unescapedLength);
+ userDataReader.consume(sampleTimeUs, userDataParsable);
+ }
+
+ if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) {
+ userData.startNalUnit(startCodeValue);
+ }
+ }
+ if (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER) {
+ int bytesWrittenPastStartCode = limit - startCodeOffset;
+ if (startedFirstSample && sampleHasPicture && hasOutputFormat) {
+ // Output the sample.
+ @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
+ int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastStartCode;
+ output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastStartCode, null);
+ }
+ if (!startedFirstSample || sampleHasPicture) {
+ // Start the next sample.
+ samplePosition = totalBytesWritten - bytesWrittenPastStartCode;
+ sampleTimeUs = pesTimeUs != C.TIME_UNSET ? pesTimeUs
+ : (startedFirstSample ? (sampleTimeUs + frameDurationUs) : 0);
+ sampleIsKeyframe = false;
+ pesTimeUs = C.TIME_UNSET;
+ startedFirstSample = true;
+ }
+ sampleHasPicture = startCodeValue == START_PICTURE;
+ } else if (startCodeValue == START_GROUP) {
+ sampleIsKeyframe = true;
+ }
+
+ offset = startCodeOffset + 3;
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Parses the {@link Format} and frame duration from a csd buffer.
+ *
+ * @param csdBuffer The csd buffer.
+ * @param formatId The id for the generated format. May be null.
+ * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or
+ * 0 if the duration could not be determined.
+ */
+ private static Pair<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer, String formatId) {
+ byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length);
+
+ int firstByte = csdData[4] & 0xFF;
+ int secondByte = csdData[5] & 0xFF;
+ int thirdByte = csdData[6] & 0xFF;
+ int width = (firstByte << 4) | (secondByte >> 4);
+ int height = (secondByte & 0x0F) << 8 | thirdByte;
+
+ float pixelWidthHeightRatio = 1f;
+ int aspectRatioCode = (csdData[7] & 0xF0) >> 4;
+ switch(aspectRatioCode) {
+ case 2:
+ pixelWidthHeightRatio = (4 * height) / (float) (3 * width);
+ break;
+ case 3:
+ pixelWidthHeightRatio = (16 * height) / (float) (9 * width);
+ break;
+ case 4:
+ pixelWidthHeightRatio = (121 * height) / (float) (100 * width);
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+
+ Format format = Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_MPEG2, null,
+ Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE,
+ Collections.singletonList(csdData), Format.NO_VALUE, pixelWidthHeightRatio, null);
+
+ long frameDurationUs = 0;
+ int frameRateCodeMinusOne = (csdData[7] & 0x0F) - 1;
+ if (0 <= frameRateCodeMinusOne && frameRateCodeMinusOne < FRAME_RATE_VALUES.length) {
+ double frameRate = FRAME_RATE_VALUES[frameRateCodeMinusOne];
+ int sequenceExtensionPosition = csdBuffer.sequenceExtensionPosition;
+ int frameRateExtensionN = (csdData[sequenceExtensionPosition + 9] & 0x60) >> 5;
+ int frameRateExtensionD = (csdData[sequenceExtensionPosition + 9] & 0x1F);
+ if (frameRateExtensionN != frameRateExtensionD) {
+ frameRate *= (frameRateExtensionN + 1d) / (frameRateExtensionD + 1);
+ }
+ frameDurationUs = (long) (C.MICROS_PER_SECOND / frameRate);
+ }
+
+ return Pair.create(format, frameDurationUs);
+ }
+
+ private static final class CsdBuffer {
+
+ private static final byte[] START_CODE = new byte[] {0, 0, 1};
+
+ private boolean isFilling;
+
+ public int length;
+ public int sequenceExtensionPosition;
+ public byte[] data;
+
+ public CsdBuffer(int initialCapacity) {
+ data = new byte[initialCapacity];
+ }
+
+ /**
+ * Resets the buffer, clearing any data that it holds.
+ */
+ public void reset() {
+ isFilling = false;
+ length = 0;
+ sequenceExtensionPosition = 0;
+ }
+
+ /**
+ * Called when a start code is encountered in the stream.
+ *
+ * @param startCodeValue The start code value.
+ * @param bytesAlreadyPassed The number of bytes of the start code that have been passed to
+ * {@link #onData(byte[], int, int)}, or 0.
+ * @return Whether the csd data is now complete. If true is returned, neither
+ * this method nor {@link #onData(byte[], int, int)} should be called again without an
+ * interleaving call to {@link #reset()}.
+ */
+ public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) {
+ if (isFilling) {
+ length -= bytesAlreadyPassed;
+ if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) {
+ sequenceExtensionPosition = length;
+ } else {
+ isFilling = false;
+ return true;
+ }
+ } else if (startCodeValue == START_SEQUENCE_HEADER) {
+ isFilling = true;
+ }
+ onData(START_CODE, 0, START_CODE.length);
+ return false;
+ }
+
+ /**
+ * Called to pass stream data.
+ *
+ * @param newData Holds the data being passed.
+ * @param offset The offset of the data in {@code data}.
+ * @param limit The limit (exclusive) of the data in {@code data}.
+ */
+ public void onData(byte[] newData, int offset, int limit) {
+ if (!isFilling) {
+ return;
+ }
+ int readLength = limit - offset;
+ if (data.length < length + readLength) {
+ data = Arrays.copyOf(data, (length + readLength) * 2);
+ }
+ System.arraycopy(newData, offset, data, length, readLength);
+ length += readLength;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java
new file mode 100644
index 0000000000..164c115159
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java
@@ -0,0 +1,567 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR;
+
+import android.util.SparseArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil.SpsData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Parses a continuous H264 byte stream and extracts individual frames.
+ */
+public final class H264Reader implements ElementaryStreamReader {
+
+ private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information
+ private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set
+ private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set
+
+ private final SeiReader seiReader;
+ private final boolean allowNonIdrKeyframes;
+ private final boolean detectAccessUnits;
+ private final NalUnitTargetBuffer sps;
+ private final NalUnitTargetBuffer pps;
+ private final NalUnitTargetBuffer sei;
+ private long totalBytesWritten;
+ private final boolean[] prefixFlags;
+
+ private String formatId;
+ private TrackOutput output;
+ private SampleReader sampleReader;
+
+ // State that should not be reset on seek.
+ private boolean hasOutputFormat;
+
+ // Per PES packet state that gets reset at the start of each PES packet.
+ private long pesTimeUs;
+
+ // State inherited from the TS packet header.
+ private boolean randomAccessIndicator;
+
+ // Scratch variables to avoid allocations.
+ private final ParsableByteArray seiWrapper;
+
+ /**
+ * @param seiReader An SEI reader for consuming closed caption channels.
+ * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as
+ * synchronization samples (key-frames).
+ * @param detectAccessUnits Whether to split the input stream into access units (samples) based on
+ * slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs).
+ */
+ public H264Reader(SeiReader seiReader, boolean allowNonIdrKeyframes, boolean detectAccessUnits) {
+ this.seiReader = seiReader;
+ this.allowNonIdrKeyframes = allowNonIdrKeyframes;
+ this.detectAccessUnits = detectAccessUnits;
+ prefixFlags = new boolean[3];
+ sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
+ pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
+ sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
+ seiWrapper = new ParsableByteArray();
+ }
+
+ @Override
+ public void seek() {
+ NalUnitUtil.clearPrefixFlags(prefixFlags);
+ sps.reset();
+ pps.reset();
+ sei.reset();
+ sampleReader.reset();
+ totalBytesWritten = 0;
+ randomAccessIndicator = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
+ sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits);
+ seiReader.createTracks(extractorOutput, idGenerator);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ this.pesTimeUs = pesTimeUs;
+ randomAccessIndicator |= (flags & FLAG_RANDOM_ACCESS_INDICATOR) != 0;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ int offset = data.getPosition();
+ int limit = data.limit();
+ byte[] dataArray = data.data;
+
+ // Append the data to the buffer.
+ totalBytesWritten += data.bytesLeft();
+ output.sampleData(data, data.bytesLeft());
+
+ // Scan the appended data, processing NAL units as they are encountered
+ while (true) {
+ int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);
+
+ if (nalUnitOffset == limit) {
+ // We've scanned to the end of the data without finding the start of another NAL unit.
+ nalUnitData(dataArray, offset, limit);
+ return;
+ }
+
+ // We've seen the start of a NAL unit of the following type.
+ int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nalUnitOffset);
+
+ // This is the number of bytes from the current offset to the start of the next NAL unit.
+ // It may be negative if the NAL unit started in the previously consumed data.
+ int lengthToNalUnit = nalUnitOffset - offset;
+ if (lengthToNalUnit > 0) {
+ nalUnitData(dataArray, offset, nalUnitOffset);
+ }
+ int bytesWrittenPastPosition = limit - nalUnitOffset;
+ long absolutePosition = totalBytesWritten - bytesWrittenPastPosition;
+ // Indicate the end of the previous NAL unit. If the length to the start of the next unit
+ // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes
+ // when notifying that the unit has ended.
+ endNalUnit(absolutePosition, bytesWrittenPastPosition,
+ lengthToNalUnit < 0 ? -lengthToNalUnit : 0, pesTimeUs);
+ // Indicate the start of the next NAL unit.
+ startNalUnit(absolutePosition, nalUnitType, pesTimeUs);
+ // Continue scanning the data.
+ offset = nalUnitOffset + 3;
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ private void startNalUnit(long position, int nalUnitType, long pesTimeUs) {
+ if (!hasOutputFormat || sampleReader.needsSpsPps()) {
+ sps.startNalUnit(nalUnitType);
+ pps.startNalUnit(nalUnitType);
+ }
+ sei.startNalUnit(nalUnitType);
+ sampleReader.startNalUnit(position, nalUnitType, pesTimeUs);
+ }
+
+ private void nalUnitData(byte[] dataArray, int offset, int limit) {
+ if (!hasOutputFormat || sampleReader.needsSpsPps()) {
+ sps.appendToNalUnit(dataArray, offset, limit);
+ pps.appendToNalUnit(dataArray, offset, limit);
+ }
+ sei.appendToNalUnit(dataArray, offset, limit);
+ sampleReader.appendToNalUnit(dataArray, offset, limit);
+ }
+
+ private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {
+ if (!hasOutputFormat || sampleReader.needsSpsPps()) {
+ sps.endNalUnit(discardPadding);
+ pps.endNalUnit(discardPadding);
+ if (!hasOutputFormat) {
+ if (sps.isCompleted() && pps.isCompleted()) {
+ List<byte[]> initializationData = new ArrayList<>();
+ initializationData.add(Arrays.copyOf(sps.nalData, sps.nalLength));
+ initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength));
+ NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength);
+ NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength);
+ output.format(
+ Format.createVideoSampleFormat(
+ formatId,
+ MimeTypes.VIDEO_H264,
+ CodecSpecificDataUtil.buildAvcCodecString(
+ spsData.profileIdc,
+ spsData.constraintsFlagsAndReservedZero2Bits,
+ spsData.levelIdc),
+ /* bitrate= */ Format.NO_VALUE,
+ /* maxInputSize= */ Format.NO_VALUE,
+ spsData.width,
+ spsData.height,
+ /* frameRate= */ Format.NO_VALUE,
+ initializationData,
+ /* rotationDegrees= */ Format.NO_VALUE,
+ spsData.pixelWidthAspectRatio,
+ /* drmInitData= */ null));
+ hasOutputFormat = true;
+ sampleReader.putSps(spsData);
+ sampleReader.putPps(ppsData);
+ sps.reset();
+ pps.reset();
+ }
+ } else if (sps.isCompleted()) {
+ NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength);
+ sampleReader.putSps(spsData);
+ sps.reset();
+ } else if (pps.isCompleted()) {
+ NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength);
+ sampleReader.putPps(ppsData);
+ pps.reset();
+ }
+ }
+ if (sei.endNalUnit(discardPadding)) {
+ int unescapedLength = NalUnitUtil.unescapeStream(sei.nalData, sei.nalLength);
+ seiWrapper.reset(sei.nalData, unescapedLength);
+ seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.
+ seiReader.consume(pesTimeUs, seiWrapper);
+ }
+ boolean sampleIsKeyFrame =
+ sampleReader.endNalUnit(position, offset, hasOutputFormat, randomAccessIndicator);
+ if (sampleIsKeyFrame) {
+ // This is either an IDR frame or the first I-frame since the random access indicator, so mark
+ // it as a keyframe. Clear the flag so that subsequent non-IDR I-frames are not marked as
+ // keyframes until we see another random access indicator.
+ randomAccessIndicator = false;
+ }
+ }
+
+ /** Consumes a stream of NAL units and outputs samples. */
+ private static final class SampleReader {
+
+ private static final int DEFAULT_BUFFER_SIZE = 128;
+
+ private static final int NAL_UNIT_TYPE_NON_IDR = 1; // Coded slice of a non-IDR picture
+ private static final int NAL_UNIT_TYPE_PARTITION_A = 2; // Coded slice data partition A
+ private static final int NAL_UNIT_TYPE_IDR = 5; // Coded slice of an IDR picture
+ private static final int NAL_UNIT_TYPE_AUD = 9; // Access unit delimiter
+
+ private final TrackOutput output;
+ private final boolean allowNonIdrKeyframes;
+ private final boolean detectAccessUnits;
+ private final SparseArray<NalUnitUtil.SpsData> sps;
+ private final SparseArray<NalUnitUtil.PpsData> pps;
+ private final ParsableNalUnitBitArray bitArray;
+
+ private byte[] buffer;
+ private int bufferLength;
+
+ // Per NAL unit state. A sample consists of one or more NAL units.
+ private int nalUnitType;
+ private long nalUnitStartPosition;
+ private boolean isFilling;
+ private long nalUnitTimeUs;
+ private SliceHeaderData previousSliceHeader;
+ private SliceHeaderData sliceHeader;
+
+ // Per sample state that gets reset at the start of each sample.
+ private boolean readingSample;
+ private long samplePosition;
+ private long sampleTimeUs;
+ private boolean sampleIsKeyframe;
+
+ public SampleReader(TrackOutput output, boolean allowNonIdrKeyframes,
+ boolean detectAccessUnits) {
+ this.output = output;
+ this.allowNonIdrKeyframes = allowNonIdrKeyframes;
+ this.detectAccessUnits = detectAccessUnits;
+ sps = new SparseArray<>();
+ pps = new SparseArray<>();
+ previousSliceHeader = new SliceHeaderData();
+ sliceHeader = new SliceHeaderData();
+ buffer = new byte[DEFAULT_BUFFER_SIZE];
+ bitArray = new ParsableNalUnitBitArray(buffer, 0, 0);
+ reset();
+ }
+
+ public boolean needsSpsPps() {
+ return detectAccessUnits;
+ }
+
+ public void putSps(NalUnitUtil.SpsData spsData) {
+ sps.append(spsData.seqParameterSetId, spsData);
+ }
+
+ public void putPps(NalUnitUtil.PpsData ppsData) {
+ pps.append(ppsData.picParameterSetId, ppsData);
+ }
+
+ public void reset() {
+ isFilling = false;
+ readingSample = false;
+ sliceHeader.clear();
+ }
+
+ public void startNalUnit(long position, int type, long pesTimeUs) {
+ nalUnitType = type;
+ nalUnitTimeUs = pesTimeUs;
+ nalUnitStartPosition = position;
+ if ((allowNonIdrKeyframes && nalUnitType == NAL_UNIT_TYPE_NON_IDR)
+ || (detectAccessUnits && (nalUnitType == NAL_UNIT_TYPE_IDR
+ || nalUnitType == NAL_UNIT_TYPE_NON_IDR
+ || nalUnitType == NAL_UNIT_TYPE_PARTITION_A))) {
+ // Store the previous header and prepare to populate the new one.
+ SliceHeaderData newSliceHeader = previousSliceHeader;
+ previousSliceHeader = sliceHeader;
+ sliceHeader = newSliceHeader;
+ sliceHeader.clear();
+ bufferLength = 0;
+ isFilling = true;
+ }
+ }
+
+ /**
+ * Called to pass stream data. The data passed should not include the 3 byte start code.
+ *
+ * @param data Holds the data being passed.
+ * @param offset The offset of the data in {@code data}.
+ * @param limit The limit (exclusive) of the data in {@code data}.
+ */
+ public void appendToNalUnit(byte[] data, int offset, int limit) {
+ if (!isFilling) {
+ return;
+ }
+ int readLength = limit - offset;
+ if (buffer.length < bufferLength + readLength) {
+ buffer = Arrays.copyOf(buffer, (bufferLength + readLength) * 2);
+ }
+ System.arraycopy(data, offset, buffer, bufferLength, readLength);
+ bufferLength += readLength;
+
+ bitArray.reset(buffer, 0, bufferLength);
+ if (!bitArray.canReadBits(8)) {
+ return;
+ }
+ bitArray.skipBit(); // forbidden_zero_bit
+ int nalRefIdc = bitArray.readBits(2);
+ bitArray.skipBits(5); // nal_unit_type
+
+ // Read the slice header using the syntax defined in ITU-T Recommendation H.264 (2013)
+ // subsection 7.3.3.
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ bitArray.readUnsignedExpGolombCodedInt(); // first_mb_in_slice
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ int sliceType = bitArray.readUnsignedExpGolombCodedInt();
+ if (!detectAccessUnits) {
+ // There are AUDs in the stream so the rest of the header can be ignored.
+ isFilling = false;
+ sliceHeader.setSliceType(sliceType);
+ return;
+ }
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ int picParameterSetId = bitArray.readUnsignedExpGolombCodedInt();
+ if (pps.indexOfKey(picParameterSetId) < 0) {
+ // We have not seen the PPS yet, so don't try to decode the slice header.
+ isFilling = false;
+ return;
+ }
+ NalUnitUtil.PpsData ppsData = pps.get(picParameterSetId);
+ NalUnitUtil.SpsData spsData = sps.get(ppsData.seqParameterSetId);
+ if (spsData.separateColorPlaneFlag) {
+ if (!bitArray.canReadBits(2)) {
+ return;
+ }
+ bitArray.skipBits(2); // colour_plane_id
+ }
+ if (!bitArray.canReadBits(spsData.frameNumLength)) {
+ return;
+ }
+ boolean fieldPicFlag = false;
+ boolean bottomFieldFlagPresent = false;
+ boolean bottomFieldFlag = false;
+ int frameNum = bitArray.readBits(spsData.frameNumLength);
+ if (!spsData.frameMbsOnlyFlag) {
+ if (!bitArray.canReadBits(1)) {
+ return;
+ }
+ fieldPicFlag = bitArray.readBit();
+ if (fieldPicFlag) {
+ if (!bitArray.canReadBits(1)) {
+ return;
+ }
+ bottomFieldFlag = bitArray.readBit();
+ bottomFieldFlagPresent = true;
+ }
+ }
+ boolean idrPicFlag = nalUnitType == NAL_UNIT_TYPE_IDR;
+ int idrPicId = 0;
+ if (idrPicFlag) {
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ idrPicId = bitArray.readUnsignedExpGolombCodedInt();
+ }
+ int picOrderCntLsb = 0;
+ int deltaPicOrderCntBottom = 0;
+ int deltaPicOrderCnt0 = 0;
+ int deltaPicOrderCnt1 = 0;
+ if (spsData.picOrderCountType == 0) {
+ if (!bitArray.canReadBits(spsData.picOrderCntLsbLength)) {
+ return;
+ }
+ picOrderCntLsb = bitArray.readBits(spsData.picOrderCntLsbLength);
+ if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) {
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ deltaPicOrderCntBottom = bitArray.readSignedExpGolombCodedInt();
+ }
+ } else if (spsData.picOrderCountType == 1
+ && !spsData.deltaPicOrderAlwaysZeroFlag) {
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ deltaPicOrderCnt0 = bitArray.readSignedExpGolombCodedInt();
+ if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) {
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ deltaPicOrderCnt1 = bitArray.readSignedExpGolombCodedInt();
+ }
+ }
+ sliceHeader.setAll(spsData, nalRefIdc, sliceType, frameNum, picParameterSetId, fieldPicFlag,
+ bottomFieldFlagPresent, bottomFieldFlag, idrPicFlag, idrPicId, picOrderCntLsb,
+ deltaPicOrderCntBottom, deltaPicOrderCnt0, deltaPicOrderCnt1);
+ isFilling = false;
+ }
+
+ public boolean endNalUnit(
+ long position, int offset, boolean hasOutputFormat, boolean randomAccessIndicator) {
+ if (nalUnitType == NAL_UNIT_TYPE_AUD
+ || (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) {
+ // If the NAL unit ending is the start of a new sample, output the previous one.
+ if (hasOutputFormat && readingSample) {
+ int nalUnitLength = (int) (position - nalUnitStartPosition);
+ outputSample(offset + nalUnitLength);
+ }
+ samplePosition = nalUnitStartPosition;
+ sampleTimeUs = nalUnitTimeUs;
+ sampleIsKeyframe = false;
+ readingSample = true;
+ }
+ boolean treatIFrameAsKeyframe =
+ allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator;
+ sampleIsKeyframe |=
+ nalUnitType == NAL_UNIT_TYPE_IDR
+ || (treatIFrameAsKeyframe && nalUnitType == NAL_UNIT_TYPE_NON_IDR);
+ return sampleIsKeyframe;
+ }
+
+ private void outputSample(int offset) {
+ @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
+ int size = (int) (nalUnitStartPosition - samplePosition);
+ output.sampleMetadata(sampleTimeUs, flags, size, offset, null);
+ }
+
+ private static final class SliceHeaderData {
+
+ private static final int SLICE_TYPE_I = 2;
+ private static final int SLICE_TYPE_ALL_I = 7;
+
+ private boolean isComplete;
+ private boolean hasSliceType;
+
+ private SpsData spsData;
+ private int nalRefIdc;
+ private int sliceType;
+ private int frameNum;
+ private int picParameterSetId;
+ private boolean fieldPicFlag;
+ private boolean bottomFieldFlagPresent;
+ private boolean bottomFieldFlag;
+ private boolean idrPicFlag;
+ private int idrPicId;
+ private int picOrderCntLsb;
+ private int deltaPicOrderCntBottom;
+ private int deltaPicOrderCnt0;
+ private int deltaPicOrderCnt1;
+
+ public void clear() {
+ hasSliceType = false;
+ isComplete = false;
+ }
+
+ public void setSliceType(int sliceType) {
+ this.sliceType = sliceType;
+ hasSliceType = true;
+ }
+
+ public void setAll(
+ SpsData spsData,
+ int nalRefIdc,
+ int sliceType,
+ int frameNum,
+ int picParameterSetId,
+ boolean fieldPicFlag,
+ boolean bottomFieldFlagPresent,
+ boolean bottomFieldFlag,
+ boolean idrPicFlag,
+ int idrPicId,
+ int picOrderCntLsb,
+ int deltaPicOrderCntBottom,
+ int deltaPicOrderCnt0,
+ int deltaPicOrderCnt1) {
+ this.spsData = spsData;
+ this.nalRefIdc = nalRefIdc;
+ this.sliceType = sliceType;
+ this.frameNum = frameNum;
+ this.picParameterSetId = picParameterSetId;
+ this.fieldPicFlag = fieldPicFlag;
+ this.bottomFieldFlagPresent = bottomFieldFlagPresent;
+ this.bottomFieldFlag = bottomFieldFlag;
+ this.idrPicFlag = idrPicFlag;
+ this.idrPicId = idrPicId;
+ this.picOrderCntLsb = picOrderCntLsb;
+ this.deltaPicOrderCntBottom = deltaPicOrderCntBottom;
+ this.deltaPicOrderCnt0 = deltaPicOrderCnt0;
+ this.deltaPicOrderCnt1 = deltaPicOrderCnt1;
+ isComplete = true;
+ hasSliceType = true;
+ }
+
+ public boolean isISlice() {
+ return hasSliceType && (sliceType == SLICE_TYPE_ALL_I || sliceType == SLICE_TYPE_I);
+ }
+
+ private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) {
+ // See ISO 14496-10 subsection 7.4.1.2.4.
+ return isComplete
+ && (!other.isComplete
+ || frameNum != other.frameNum
+ || picParameterSetId != other.picParameterSetId
+ || fieldPicFlag != other.fieldPicFlag
+ || (bottomFieldFlagPresent
+ && other.bottomFieldFlagPresent
+ && bottomFieldFlag != other.bottomFieldFlag)
+ || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
+ || (spsData.picOrderCountType == 0
+ && other.spsData.picOrderCountType == 0
+ && (picOrderCntLsb != other.picOrderCntLsb
+ || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom))
+ || (spsData.picOrderCountType == 1
+ && other.spsData.picOrderCountType == 1
+ && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0
+ || deltaPicOrderCnt1 != other.deltaPicOrderCnt1))
+ || idrPicFlag != other.idrPicFlag
+ || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId));
+ }
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java
new file mode 100644
index 0000000000..6aa7c5d71d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java
@@ -0,0 +1,494 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
+import java.util.Collections;
+
+/**
+ * Parses a continuous H.265 byte stream and extracts individual frames.
+ */
+public final class H265Reader implements ElementaryStreamReader {
+
+ private static final String TAG = "H265Reader";
+
+ // nal_unit_type values from H.265/HEVC (2014) Table 7-1.
+ private static final int RASL_R = 9;
+ private static final int BLA_W_LP = 16;
+ private static final int CRA_NUT = 21;
+ private static final int VPS_NUT = 32;
+ private static final int SPS_NUT = 33;
+ private static final int PPS_NUT = 34;
+ private static final int PREFIX_SEI_NUT = 39;
+ private static final int SUFFIX_SEI_NUT = 40;
+
+ private final SeiReader seiReader;
+
+ private String formatId;
+ private TrackOutput output;
+ private SampleReader sampleReader;
+
+ // State that should not be reset on seek.
+ private boolean hasOutputFormat;
+
+ // State that should be reset on seek.
+ private final boolean[] prefixFlags;
+ private final NalUnitTargetBuffer vps;
+ private final NalUnitTargetBuffer sps;
+ private final NalUnitTargetBuffer pps;
+ private final NalUnitTargetBuffer prefixSei;
+ private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed?
+ private long totalBytesWritten;
+
+ // Per packet state that gets reset at the start of each packet.
+ private long pesTimeUs;
+
+ // Scratch variables to avoid allocations.
+ private final ParsableByteArray seiWrapper;
+
+ /**
+ * @param seiReader An SEI reader for consuming closed caption channels.
+ */
+ public H265Reader(SeiReader seiReader) {
+ this.seiReader = seiReader;
+ prefixFlags = new boolean[3];
+ vps = new NalUnitTargetBuffer(VPS_NUT, 128);
+ sps = new NalUnitTargetBuffer(SPS_NUT, 128);
+ pps = new NalUnitTargetBuffer(PPS_NUT, 128);
+ prefixSei = new NalUnitTargetBuffer(PREFIX_SEI_NUT, 128);
+ suffixSei = new NalUnitTargetBuffer(SUFFIX_SEI_NUT, 128);
+ seiWrapper = new ParsableByteArray();
+ }
+
+ @Override
+ public void seek() {
+ NalUnitUtil.clearPrefixFlags(prefixFlags);
+ vps.reset();
+ sps.reset();
+ pps.reset();
+ prefixSei.reset();
+ suffixSei.reset();
+ sampleReader.reset();
+ totalBytesWritten = 0;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
+ sampleReader = new SampleReader(output);
+ seiReader.createTracks(extractorOutput, idGenerator);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ // TODO (Internal b/32267012): Consider using random access indicator.
+ this.pesTimeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ while (data.bytesLeft() > 0) {
+ int offset = data.getPosition();
+ int limit = data.limit();
+ byte[] dataArray = data.data;
+
+ // Append the data to the buffer.
+ totalBytesWritten += data.bytesLeft();
+ output.sampleData(data, data.bytesLeft());
+
+ // Scan the appended data, processing NAL units as they are encountered
+ while (offset < limit) {
+ int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);
+
+ if (nalUnitOffset == limit) {
+ // We've scanned to the end of the data without finding the start of another NAL unit.
+ nalUnitData(dataArray, offset, limit);
+ return;
+ }
+
+ // We've seen the start of a NAL unit of the following type.
+ int nalUnitType = NalUnitUtil.getH265NalUnitType(dataArray, nalUnitOffset);
+
+ // This is the number of bytes from the current offset to the start of the next NAL unit.
+ // It may be negative if the NAL unit started in the previously consumed data.
+ int lengthToNalUnit = nalUnitOffset - offset;
+ if (lengthToNalUnit > 0) {
+ nalUnitData(dataArray, offset, nalUnitOffset);
+ }
+
+ int bytesWrittenPastPosition = limit - nalUnitOffset;
+ long absolutePosition = totalBytesWritten - bytesWrittenPastPosition;
+ // Indicate the end of the previous NAL unit. If the length to the start of the next unit
+ // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes
+ // when notifying that the unit has ended.
+ endNalUnit(absolutePosition, bytesWrittenPastPosition,
+ lengthToNalUnit < 0 ? -lengthToNalUnit : 0, pesTimeUs);
+ // Indicate the start of the next NAL unit.
+ startNalUnit(absolutePosition, bytesWrittenPastPosition, nalUnitType, pesTimeUs);
+ // Continue scanning the data.
+ offset = nalUnitOffset + 3;
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) {
+ if (hasOutputFormat) {
+ sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs);
+ } else {
+ vps.startNalUnit(nalUnitType);
+ sps.startNalUnit(nalUnitType);
+ pps.startNalUnit(nalUnitType);
+ }
+ prefixSei.startNalUnit(nalUnitType);
+ suffixSei.startNalUnit(nalUnitType);
+ }
+
+ private void nalUnitData(byte[] dataArray, int offset, int limit) {
+ if (hasOutputFormat) {
+ sampleReader.readNalUnitData(dataArray, offset, limit);
+ } else {
+ vps.appendToNalUnit(dataArray, offset, limit);
+ sps.appendToNalUnit(dataArray, offset, limit);
+ pps.appendToNalUnit(dataArray, offset, limit);
+ }
+ prefixSei.appendToNalUnit(dataArray, offset, limit);
+ suffixSei.appendToNalUnit(dataArray, offset, limit);
+ }
+
+ private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {
+ if (hasOutputFormat) {
+ sampleReader.endNalUnit(position, offset);
+ } else {
+ vps.endNalUnit(discardPadding);
+ sps.endNalUnit(discardPadding);
+ pps.endNalUnit(discardPadding);
+ if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) {
+ output.format(parseMediaFormat(formatId, vps, sps, pps));
+ hasOutputFormat = true;
+ }
+ }
+ if (prefixSei.endNalUnit(discardPadding)) {
+ int unescapedLength = NalUnitUtil.unescapeStream(prefixSei.nalData, prefixSei.nalLength);
+ seiWrapper.reset(prefixSei.nalData, unescapedLength);
+
+ // Skip the NAL prefix and type.
+ seiWrapper.skipBytes(5);
+ seiReader.consume(pesTimeUs, seiWrapper);
+ }
+ if (suffixSei.endNalUnit(discardPadding)) {
+ int unescapedLength = NalUnitUtil.unescapeStream(suffixSei.nalData, suffixSei.nalLength);
+ seiWrapper.reset(suffixSei.nalData, unescapedLength);
+
+ // Skip the NAL prefix and type.
+ seiWrapper.skipBytes(5);
+ seiReader.consume(pesTimeUs, seiWrapper);
+ }
+ }
+
+ private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps,
+ NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) {
+ // Build codec-specific data.
+ byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength];
+ System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength);
+ System.arraycopy(sps.nalData, 0, csd, vps.nalLength, sps.nalLength);
+ System.arraycopy(pps.nalData, 0, csd, vps.nalLength + sps.nalLength, pps.nalLength);
+
+ // Parse the SPS NAL unit, as per H.265/HEVC (2014) 7.3.2.2.1.
+ ParsableNalUnitBitArray bitArray = new ParsableNalUnitBitArray(sps.nalData, 0, sps.nalLength);
+ bitArray.skipBits(40 + 4); // NAL header, sps_video_parameter_set_id
+ int maxSubLayersMinus1 = bitArray.readBits(3);
+ bitArray.skipBit(); // sps_temporal_id_nesting_flag
+
+ // profile_tier_level(1, sps_max_sub_layers_minus1)
+ bitArray.skipBits(88); // if (profilePresentFlag) {...}
+ bitArray.skipBits(8); // general_level_idc
+ int toSkip = 0;
+ for (int i = 0; i < maxSubLayersMinus1; i++) {
+ if (bitArray.readBit()) { // sub_layer_profile_present_flag[i]
+ toSkip += 89;
+ }
+ if (bitArray.readBit()) { // sub_layer_level_present_flag[i]
+ toSkip += 8;
+ }
+ }
+ bitArray.skipBits(toSkip);
+ if (maxSubLayersMinus1 > 0) {
+ bitArray.skipBits(2 * (8 - maxSubLayersMinus1));
+ }
+
+ bitArray.readUnsignedExpGolombCodedInt(); // sps_seq_parameter_set_id
+ int chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt();
+ if (chromaFormatIdc == 3) {
+ bitArray.skipBit(); // separate_colour_plane_flag
+ }
+ int picWidthInLumaSamples = bitArray.readUnsignedExpGolombCodedInt();
+ int picHeightInLumaSamples = bitArray.readUnsignedExpGolombCodedInt();
+ if (bitArray.readBit()) { // conformance_window_flag
+ int confWinLeftOffset = bitArray.readUnsignedExpGolombCodedInt();
+ int confWinRightOffset = bitArray.readUnsignedExpGolombCodedInt();
+ int confWinTopOffset = bitArray.readUnsignedExpGolombCodedInt();
+ int confWinBottomOffset = bitArray.readUnsignedExpGolombCodedInt();
+ // H.265/HEVC (2014) Table 6-1
+ int subWidthC = chromaFormatIdc == 1 || chromaFormatIdc == 2 ? 2 : 1;
+ int subHeightC = chromaFormatIdc == 1 ? 2 : 1;
+ picWidthInLumaSamples -= subWidthC * (confWinLeftOffset + confWinRightOffset);
+ picHeightInLumaSamples -= subHeightC * (confWinTopOffset + confWinBottomOffset);
+ }
+ bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8
+ bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8
+ int log2MaxPicOrderCntLsbMinus4 = bitArray.readUnsignedExpGolombCodedInt();
+ // for (i = sps_sub_layer_ordering_info_present_flag ? 0 : sps_max_sub_layers_minus1; ...)
+ for (int i = bitArray.readBit() ? 0 : maxSubLayersMinus1; i <= maxSubLayersMinus1; i++) {
+ bitArray.readUnsignedExpGolombCodedInt(); // sps_max_dec_pic_buffering_minus1[i]
+ bitArray.readUnsignedExpGolombCodedInt(); // sps_max_num_reorder_pics[i]
+ bitArray.readUnsignedExpGolombCodedInt(); // sps_max_latency_increase_plus1[i]
+ }
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_coding_block_size_minus3
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_coding_block_size
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_transform_block_size_minus2
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_transform_block_size
+ bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_inter
+ bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_intra
+ // if (scaling_list_enabled_flag) { if (sps_scaling_list_data_present_flag) {...}}
+ boolean scalingListEnabled = bitArray.readBit();
+ if (scalingListEnabled && bitArray.readBit()) {
+ skipScalingList(bitArray);
+ }
+ bitArray.skipBits(2); // amp_enabled_flag (1), sample_adaptive_offset_enabled_flag (1)
+ if (bitArray.readBit()) { // pcm_enabled_flag
+ // pcm_sample_bit_depth_luma_minus1 (4), pcm_sample_bit_depth_chroma_minus1 (4)
+ bitArray.skipBits(8);
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_min_pcm_luma_coding_block_size_minus3
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_pcm_luma_coding_block_size
+ bitArray.skipBit(); // pcm_loop_filter_disabled_flag
+ }
+ // Skips all short term reference picture sets.
+ skipShortTermRefPicSets(bitArray);
+ if (bitArray.readBit()) { // long_term_ref_pics_present_flag
+ // num_long_term_ref_pics_sps
+ for (int i = 0; i < bitArray.readUnsignedExpGolombCodedInt(); i++) {
+ int ltRefPicPocLsbSpsLength = log2MaxPicOrderCntLsbMinus4 + 4;
+ // lt_ref_pic_poc_lsb_sps[i], used_by_curr_pic_lt_sps_flag[i]
+ bitArray.skipBits(ltRefPicPocLsbSpsLength + 1);
+ }
+ }
+ bitArray.skipBits(2); // sps_temporal_mvp_enabled_flag, strong_intra_smoothing_enabled_flag
+ float pixelWidthHeightRatio = 1;
+ if (bitArray.readBit()) { // vui_parameters_present_flag
+ if (bitArray.readBit()) { // aspect_ratio_info_present_flag
+ int aspectRatioIdc = bitArray.readBits(8);
+ if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) {
+ int sarWidth = bitArray.readBits(16);
+ int sarHeight = bitArray.readBits(16);
+ if (sarWidth != 0 && sarHeight != 0) {
+ pixelWidthHeightRatio = (float) sarWidth / sarHeight;
+ }
+ } else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) {
+ pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc];
+ } else {
+ Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc);
+ }
+ }
+ }
+
+ return Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H265, null, Format.NO_VALUE,
+ Format.NO_VALUE, picWidthInLumaSamples, picHeightInLumaSamples, Format.NO_VALUE,
+ Collections.singletonList(csd), Format.NO_VALUE, pixelWidthHeightRatio, null);
+ }
+
+ /**
+ * Skips scaling_list_data(). See H.265/HEVC (2014) 7.3.4.
+ */
+ private static void skipScalingList(ParsableNalUnitBitArray bitArray) {
+ for (int sizeId = 0; sizeId < 4; sizeId++) {
+ for (int matrixId = 0; matrixId < 6; matrixId += sizeId == 3 ? 3 : 1) {
+ if (!bitArray.readBit()) { // scaling_list_pred_mode_flag[sizeId][matrixId]
+ // scaling_list_pred_matrix_id_delta[sizeId][matrixId]
+ bitArray.readUnsignedExpGolombCodedInt();
+ } else {
+ int coefNum = Math.min(64, 1 << (4 + (sizeId << 1)));
+ if (sizeId > 1) {
+ // scaling_list_dc_coef_minus8[sizeId - 2][matrixId]
+ bitArray.readSignedExpGolombCodedInt();
+ }
+ for (int i = 0; i < coefNum; i++) {
+ bitArray.readSignedExpGolombCodedInt(); // scaling_list_delta_coef
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads the number of short term reference picture sets in a SPS as ue(v), then skips all of
+ * them. See H.265/HEVC (2014) 7.3.7.
+ */
+ private static void skipShortTermRefPicSets(ParsableNalUnitBitArray bitArray) {
+ int numShortTermRefPicSets = bitArray.readUnsignedExpGolombCodedInt();
+ boolean interRefPicSetPredictionFlag = false;
+ int numNegativePics;
+ int numPositivePics;
+ // As this method applies in a SPS, the only element of NumDeltaPocs accessed is the previous
+ // one, so we just keep track of that rather than storing the whole array.
+ // RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1) and delta_idx_minus1 is always zero in SPS.
+ int previousNumDeltaPocs = 0;
+ for (int stRpsIdx = 0; stRpsIdx < numShortTermRefPicSets; stRpsIdx++) {
+ if (stRpsIdx != 0) {
+ interRefPicSetPredictionFlag = bitArray.readBit();
+ }
+ if (interRefPicSetPredictionFlag) {
+ bitArray.skipBit(); // delta_rps_sign
+ bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1
+ for (int j = 0; j <= previousNumDeltaPocs; j++) {
+ if (bitArray.readBit()) { // used_by_curr_pic_flag[j]
+ bitArray.skipBit(); // use_delta_flag[j]
+ }
+ }
+ } else {
+ numNegativePics = bitArray.readUnsignedExpGolombCodedInt();
+ numPositivePics = bitArray.readUnsignedExpGolombCodedInt();
+ previousNumDeltaPocs = numNegativePics + numPositivePics;
+ for (int i = 0; i < numNegativePics; i++) {
+ bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[i]
+ bitArray.skipBit(); // used_by_curr_pic_s0_flag[i]
+ }
+ for (int i = 0; i < numPositivePics; i++) {
+ bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i]
+ bitArray.skipBit(); // used_by_curr_pic_s1_flag[i]
+ }
+ }
+ }
+ }
+
+ private static final class SampleReader {
+
+ /**
+ * Offset in bytes of the first_slice_segment_in_pic_flag in a NAL unit containing a
+ * slice_segment_layer_rbsp.
+ */
+ private static final int FIRST_SLICE_FLAG_OFFSET = 2;
+
+ private final TrackOutput output;
+
+ // Per NAL unit state. A sample consists of one or more NAL units.
+ private long nalUnitStartPosition;
+ private boolean nalUnitHasKeyframeData;
+ private int nalUnitBytesRead;
+ private long nalUnitTimeUs;
+ private boolean lookingForFirstSliceFlag;
+ private boolean isFirstSlice;
+ private boolean isFirstParameterSet;
+
+ // Per sample state that gets reset at the start of each sample.
+ private boolean readingSample;
+ private boolean writingParameterSets;
+ private long samplePosition;
+ private long sampleTimeUs;
+ private boolean sampleIsKeyframe;
+
+ public SampleReader(TrackOutput output) {
+ this.output = output;
+ }
+
+ public void reset() {
+ lookingForFirstSliceFlag = false;
+ isFirstSlice = false;
+ isFirstParameterSet = false;
+ readingSample = false;
+ writingParameterSets = false;
+ }
+
+ public void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) {
+ isFirstSlice = false;
+ isFirstParameterSet = false;
+ nalUnitTimeUs = pesTimeUs;
+ nalUnitBytesRead = 0;
+ nalUnitStartPosition = position;
+
+ if (nalUnitType >= VPS_NUT) {
+ if (!writingParameterSets && readingSample) {
+ // This is a non-VCL NAL unit, so flush the previous sample.
+ outputSample(offset);
+ readingSample = false;
+ }
+ if (nalUnitType <= PPS_NUT) {
+ // This sample will have parameter sets at the start.
+ isFirstParameterSet = !writingParameterSets;
+ writingParameterSets = true;
+ }
+ }
+
+ // Look for the flag if this NAL unit contains a slice_segment_layer_rbsp.
+ nalUnitHasKeyframeData = (nalUnitType >= BLA_W_LP && nalUnitType <= CRA_NUT);
+ lookingForFirstSliceFlag = nalUnitHasKeyframeData || nalUnitType <= RASL_R;
+ }
+
+ public void readNalUnitData(byte[] data, int offset, int limit) {
+ if (lookingForFirstSliceFlag) {
+ int headerOffset = offset + FIRST_SLICE_FLAG_OFFSET - nalUnitBytesRead;
+ if (headerOffset < limit) {
+ isFirstSlice = (data[headerOffset] & 0x80) != 0;
+ lookingForFirstSliceFlag = false;
+ } else {
+ nalUnitBytesRead += limit - offset;
+ }
+ }
+ }
+
+ public void endNalUnit(long position, int offset) {
+ if (writingParameterSets && isFirstSlice) {
+ // This sample has parameter sets. Reset the key-frame flag based on the first slice.
+ sampleIsKeyframe = nalUnitHasKeyframeData;
+ writingParameterSets = false;
+ } else if (isFirstParameterSet || isFirstSlice) {
+ // This NAL unit is at the start of a new sample (access unit).
+ if (readingSample) {
+ // Output the sample ending before this NAL unit.
+ int nalUnitLength = (int) (position - nalUnitStartPosition);
+ outputSample(offset + nalUnitLength);
+ }
+ samplePosition = nalUnitStartPosition;
+ sampleTimeUs = nalUnitTimeUs;
+ readingSample = true;
+ sampleIsKeyframe = nalUnitHasKeyframeData;
+ }
+ }
+
+ private void outputSample(int offset) {
+ @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
+ int size = (int) (nalUnitStartPosition - samplePosition);
+ output.sampleMetadata(sampleTimeUs, flags, size, offset, null);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java
new file mode 100644
index 0000000000..da63e143c2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java
@@ -0,0 +1,116 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Parses ID3 data and extracts individual text information frames.
+ */
+public final class Id3Reader implements ElementaryStreamReader {
+
+ private static final String TAG = "Id3Reader";
+
+ private final ParsableByteArray id3Header;
+
+ private TrackOutput output;
+
+ // State that should be reset on seek.
+ private boolean writingSample;
+
+ // Per sample state that gets reset at the start of each sample.
+ private long sampleTimeUs;
+ private int sampleSize;
+ private int sampleBytesRead;
+
+ public Id3Reader() {
+ id3Header = new ParsableByteArray(ID3_HEADER_LENGTH);
+ }
+
+ @Override
+ public void seek() {
+ writingSample = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
+ output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3,
+ null, Format.NO_VALUE, null));
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
+ return;
+ }
+ writingSample = true;
+ sampleTimeUs = pesTimeUs;
+ sampleSize = 0;
+ sampleBytesRead = 0;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ if (!writingSample) {
+ return;
+ }
+ int bytesAvailable = data.bytesLeft();
+ if (sampleBytesRead < ID3_HEADER_LENGTH) {
+ // We're still reading the ID3 header.
+ int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_LENGTH - sampleBytesRead);
+ System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead,
+ headerBytesAvailable);
+ if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_LENGTH) {
+ // We've finished reading the ID3 header. Extract the sample size.
+ id3Header.setPosition(0);
+ if ('I' != id3Header.readUnsignedByte() || 'D' != id3Header.readUnsignedByte()
+ || '3' != id3Header.readUnsignedByte()) {
+ Log.w(TAG, "Discarding invalid ID3 tag");
+ writingSample = false;
+ return;
+ }
+ id3Header.skipBytes(3); // version (2) + flags (1)
+ sampleSize = ID3_HEADER_LENGTH + id3Header.readSynchSafeInt();
+ }
+ }
+ // Write data to the output.
+ int bytesToWrite = Math.min(bytesAvailable, sampleSize - sampleBytesRead);
+ output.sampleData(data, bytesToWrite);
+ sampleBytesRead += bytesToWrite;
+ }
+
+ @Override
+ public void packetFinished() {
+ if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) {
+ return;
+ }
+ output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ writingSample = false;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java
new file mode 100644
index 0000000000..1a41adfa69
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Collections;
+
+/**
+ * Parses and extracts samples from an AAC/LATM elementary stream.
+ */
+public final class LatmReader implements ElementaryStreamReader {
+
+ private static final int STATE_FINDING_SYNC_1 = 0;
+ private static final int STATE_FINDING_SYNC_2 = 1;
+ private static final int STATE_READING_HEADER = 2;
+ private static final int STATE_READING_SAMPLE = 3;
+
+ private static final int INITIAL_BUFFER_SIZE = 1024;
+ private static final int SYNC_BYTE_FIRST = 0x56;
+ private static final int SYNC_BYTE_SECOND = 0xE0;
+
+ private final String language;
+ private final ParsableByteArray sampleDataBuffer;
+ private final ParsableBitArray sampleBitArray;
+
+ // Track output info.
+ private TrackOutput output;
+ private Format format;
+ private String formatId;
+
+ // Parser state info.
+ private int state;
+ private int bytesRead;
+ private int sampleSize;
+ private int secondHeaderByte;
+ private long timeUs;
+
+ // Container data.
+ private boolean streamMuxRead;
+ private int audioMuxVersionA;
+ private int numSubframes;
+ private int frameLengthType;
+ private boolean otherDataPresent;
+ private long otherDataLenBits;
+ private int sampleRateHz;
+ private long sampleDurationUs;
+ private int channelCount;
+
+ /**
+ * @param language Track language.
+ */
+ public LatmReader(@Nullable String language) {
+ this.language = language;
+ sampleDataBuffer = new ParsableByteArray(INITIAL_BUFFER_SIZE);
+ sampleBitArray = new ParsableBitArray(sampleDataBuffer.data);
+ }
+
+ @Override
+ public void seek() {
+ state = STATE_FINDING_SYNC_1;
+ streamMuxRead = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
+ formatId = idGenerator.getFormatId();
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ timeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) throws ParserException {
+ int bytesToRead;
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_SYNC_1:
+ if (data.readUnsignedByte() == SYNC_BYTE_FIRST) {
+ state = STATE_FINDING_SYNC_2;
+ }
+ break;
+ case STATE_FINDING_SYNC_2:
+ int secondByte = data.readUnsignedByte();
+ if ((secondByte & SYNC_BYTE_SECOND) == SYNC_BYTE_SECOND) {
+ secondHeaderByte = secondByte;
+ state = STATE_READING_HEADER;
+ } else if (secondByte != SYNC_BYTE_FIRST) {
+ state = STATE_FINDING_SYNC_1;
+ }
+ break;
+ case STATE_READING_HEADER:
+ sampleSize = ((secondHeaderByte & ~SYNC_BYTE_SECOND) << 8) | data.readUnsignedByte();
+ if (sampleSize > sampleDataBuffer.data.length) {
+ resetBufferForSize(sampleSize);
+ }
+ bytesRead = 0;
+ state = STATE_READING_SAMPLE;
+ break;
+ case STATE_READING_SAMPLE:
+ bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
+ data.readBytes(sampleBitArray.data, bytesRead, bytesToRead);
+ bytesRead += bytesToRead;
+ if (bytesRead == sampleSize) {
+ sampleBitArray.setPosition(0);
+ parseAudioMuxElement(sampleBitArray);
+ state = STATE_FINDING_SYNC_1;
+ }
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Parses an AudioMuxElement as defined in 14496-3:2009, Section 1.7.3.1, Table 1.41.
+ *
+ * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes.
+ */
+ private void parseAudioMuxElement(ParsableBitArray data) throws ParserException {
+ boolean useSameStreamMux = data.readBit();
+ if (!useSameStreamMux) {
+ streamMuxRead = true;
+ parseStreamMuxConfig(data);
+ } else if (!streamMuxRead) {
+ return; // Parsing cannot continue without StreamMuxConfig information.
+ }
+
+ if (audioMuxVersionA == 0) {
+ if (numSubframes != 0) {
+ throw new ParserException();
+ }
+ int muxSlotLengthBytes = parsePayloadLengthInfo(data);
+ parsePayloadMux(data, muxSlotLengthBytes);
+ if (otherDataPresent) {
+ data.skipBits((int) otherDataLenBits);
+ }
+ } else {
+ throw new ParserException(); // Not defined by ISO/IEC 14496-3:2009.
+ }
+ }
+
+ /**
+ * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42.
+ */
+ private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException {
+ int audioMuxVersion = data.readBits(1);
+ audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0;
+ if (audioMuxVersionA == 0) {
+ if (audioMuxVersion == 1) {
+ latmGetValue(data); // Skip taraBufferFullness.
+ }
+ if (!data.readBit()) {
+ throw new ParserException();
+ }
+ numSubframes = data.readBits(6);
+ int numProgram = data.readBits(4);
+ int numLayer = data.readBits(3);
+ if (numProgram != 0 || numLayer != 0) {
+ throw new ParserException();
+ }
+ if (audioMuxVersion == 0) {
+ int startPosition = data.getPosition();
+ int readBits = parseAudioSpecificConfig(data);
+ data.setPosition(startPosition);
+ byte[] initData = new byte[(readBits + 7) / 8];
+ data.readBits(initData, 0, readBits);
+ Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null,
+ Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz,
+ Collections.singletonList(initData), null, 0, language);
+ if (!format.equals(this.format)) {
+ this.format = format;
+ sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate;
+ output.format(format);
+ }
+ } else {
+ int ascLen = (int) latmGetValue(data);
+ int bitsRead = parseAudioSpecificConfig(data);
+ data.skipBits(ascLen - bitsRead); // fillBits.
+ }
+ parseFrameLength(data);
+ otherDataPresent = data.readBit();
+ otherDataLenBits = 0;
+ if (otherDataPresent) {
+ if (audioMuxVersion == 1) {
+ otherDataLenBits = latmGetValue(data);
+ } else {
+ boolean otherDataLenEsc;
+ do {
+ otherDataLenEsc = data.readBit();
+ otherDataLenBits = (otherDataLenBits << 8) + data.readBits(8);
+ } while (otherDataLenEsc);
+ }
+ }
+ boolean crcCheckPresent = data.readBit();
+ if (crcCheckPresent) {
+ data.skipBits(8); // crcCheckSum.
+ }
+ } else {
+ throw new ParserException(); // This is not defined by ISO/IEC 14496-3:2009.
+ }
+ }
+
+ private void parseFrameLength(ParsableBitArray data) {
+ frameLengthType = data.readBits(3);
+ switch (frameLengthType) {
+ case 0:
+ data.skipBits(8); // latmBufferFullness.
+ break;
+ case 1:
+ data.skipBits(9); // frameLength.
+ break;
+ case 3:
+ case 4:
+ case 5:
+ data.skipBits(6); // CELPframeLengthTableIndex.
+ break;
+ case 6:
+ case 7:
+ data.skipBits(1); // HVXCframeLengthTableIndex.
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ private int parseAudioSpecificConfig(ParsableBitArray data) throws ParserException {
+ int bitsLeft = data.bitsLeft();
+ Pair<Integer, Integer> config = CodecSpecificDataUtil.parseAacAudioSpecificConfig(data, true);
+ sampleRateHz = config.first;
+ channelCount = config.second;
+ return bitsLeft - data.bitsLeft();
+ }
+
+ private int parsePayloadLengthInfo(ParsableBitArray data) throws ParserException {
+ int muxSlotLengthBytes = 0;
+ // Assuming single program and single layer.
+ if (frameLengthType == 0) {
+ int tmp;
+ do {
+ tmp = data.readBits(8);
+ muxSlotLengthBytes += tmp;
+ } while (tmp == 255);
+ return muxSlotLengthBytes;
+ } else {
+ throw new ParserException();
+ }
+ }
+
+ private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) {
+ // The start of sample data in
+ int bitPosition = data.getPosition();
+ if ((bitPosition & 0x07) == 0) {
+ // Sample data is byte-aligned. We can output it directly.
+ sampleDataBuffer.setPosition(bitPosition >> 3);
+ } else {
+ // Sample data is not byte-aligned and we need align it ourselves before outputting.
+ // Byte alignment is needed because LATM framing is not supported by MediaCodec.
+ data.readBits(sampleDataBuffer.data, 0, muxLengthBytes * 8);
+ sampleDataBuffer.setPosition(0);
+ }
+ output.sampleData(sampleDataBuffer, muxLengthBytes);
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, muxLengthBytes, 0, null);
+ timeUs += sampleDurationUs;
+ }
+
+ private void resetBufferForSize(int newSize) {
+ sampleDataBuffer.reset(newSize);
+ sampleBitArray.reset(sampleDataBuffer.data);
+ }
+
+ private static long latmGetValue(ParsableBitArray data) {
+ int bytesForValue = data.readBits(2);
+ return data.readBits((bytesForValue + 1) * 8);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java
new file mode 100644
index 0000000000..6fefab6314
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java
@@ -0,0 +1,223 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Parses a continuous MPEG Audio byte stream and extracts individual frames.
+ */
+public final class MpegAudioReader implements ElementaryStreamReader {
+
+ private static final int STATE_FINDING_HEADER = 0;
+ private static final int STATE_READING_HEADER = 1;
+ private static final int STATE_READING_FRAME = 2;
+
+ private static final int HEADER_SIZE = 4;
+
+ private final ParsableByteArray headerScratch;
+ private final MpegAudioHeader header;
+ private final String language;
+
+ private String formatId;
+ private TrackOutput output;
+
+ private int state;
+ private int frameBytesRead;
+ private boolean hasOutputFormat;
+
+ // Used when finding the frame header.
+ private boolean lastByteWasFF;
+
+ // Parsed from the frame header.
+ private long frameDurationUs;
+ private int frameSize;
+
+ // The timestamp to attach to the next sample in the current packet.
+ private long timeUs;
+
+ public MpegAudioReader() {
+ this(null);
+ }
+
+ public MpegAudioReader(String language) {
+ state = STATE_FINDING_HEADER;
+ // The first byte of an MPEG Audio frame header is always 0xFF.
+ headerScratch = new ParsableByteArray(4);
+ headerScratch.data[0] = (byte) 0xFF;
+ header = new MpegAudioHeader();
+ this.language = language;
+ }
+
+ @Override
+ public void seek() {
+ state = STATE_FINDING_HEADER;
+ frameBytesRead = 0;
+ lastByteWasFF = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ timeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_HEADER:
+ findHeader(data);
+ break;
+ case STATE_READING_HEADER:
+ readHeaderRemainder(data);
+ break;
+ case STATE_READING_FRAME:
+ readFrameRemainder(data);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Attempts to locate the start of the next frame header.
+ * <p>
+ * If a frame header is located then the state is changed to {@link #STATE_READING_HEADER}, the
+ * first two bytes of the header are written into {@link #headerScratch}, and the position of the
+ * source is advanced to the byte that immediately follows these two bytes.
+ * <p>
+ * If a frame header is not located then the position of the source is advanced to the limit, and
+ * the method should be called again with the next source to continue the search.
+ *
+ * @param source The source from which to read.
+ */
+ private void findHeader(ParsableByteArray source) {
+ byte[] data = source.data;
+ int startOffset = source.getPosition();
+ int endOffset = source.limit();
+ for (int i = startOffset; i < endOffset; i++) {
+ boolean byteIsFF = (data[i] & 0xFF) == 0xFF;
+ boolean found = lastByteWasFF && (data[i] & 0xE0) == 0xE0;
+ lastByteWasFF = byteIsFF;
+ if (found) {
+ source.setPosition(i + 1);
+ // Reset lastByteWasFF for next time.
+ lastByteWasFF = false;
+ headerScratch.data[1] = data[i];
+ frameBytesRead = 2;
+ state = STATE_READING_HEADER;
+ return;
+ }
+ }
+ source.setPosition(endOffset);
+ }
+
+ /**
+ * Attempts to read the remaining two bytes of the frame header.
+ * <p>
+ * If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME},
+ * the media format is output if this has not previously occurred, the four header bytes are
+ * output as sample data, and the position of the source is advanced to the byte that immediately
+ * follows the header.
+ * <p>
+ * If a frame header is read in full but cannot be parsed then the state is changed to
+ * {@link #STATE_READING_HEADER}.
+ * <p>
+ * If a frame header is not read in full then the position of the source is advanced to the limit,
+ * and the method should be called again with the next source to continue the read.
+ *
+ * @param source The source from which to read.
+ */
+ private void readHeaderRemainder(ParsableByteArray source) {
+ int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead);
+ source.readBytes(headerScratch.data, frameBytesRead, bytesToRead);
+ frameBytesRead += bytesToRead;
+ if (frameBytesRead < HEADER_SIZE) {
+ // We haven't read the whole header yet.
+ return;
+ }
+
+ headerScratch.setPosition(0);
+ boolean parsedHeader = MpegAudioHeader.populateHeader(headerScratch.readInt(), header);
+ if (!parsedHeader) {
+ // We thought we'd located a frame header, but we hadn't.
+ frameBytesRead = 0;
+ state = STATE_READING_HEADER;
+ return;
+ }
+
+ frameSize = header.frameSize;
+ if (!hasOutputFormat) {
+ frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate;
+ Format format = Format.createAudioSampleFormat(formatId, header.mimeType, null,
+ Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate,
+ null, null, 0, language);
+ output.format(format);
+ hasOutputFormat = true;
+ }
+
+ headerScratch.setPosition(0);
+ output.sampleData(headerScratch, HEADER_SIZE);
+ state = STATE_READING_FRAME;
+ }
+
+ /**
+ * Attempts to read the remainder of the frame.
+ * <p>
+ * If a frame is read in full then true is returned. The frame will have been output, and the
+ * position of the source will have been advanced to the byte that immediately follows the end of
+ * the frame.
+ * <p>
+ * If a frame is not read in full then the position of the source will have been advanced to the
+ * limit, and the method should be called again with the next source to continue the read.
+ *
+ * @param source The source from which to read.
+ */
+ private void readFrameRemainder(ParsableByteArray source) {
+ int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead);
+ output.sampleData(source, bytesToRead);
+ frameBytesRead += bytesToRead;
+ if (frameBytesRead < frameSize) {
+ // We haven't read the whole of the frame yet.
+ return;
+ }
+
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, frameSize, 0, null);
+ timeUs += frameDurationUs;
+ frameBytesRead = 0;
+ state = STATE_FINDING_HEADER;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java
new file mode 100644
index 0000000000..4941aa29a0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java
@@ -0,0 +1,109 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.Arrays;
+
+/**
+ * A buffer that fills itself with data corresponding to a specific NAL unit, as it is
+ * encountered in the stream.
+ */
+/* package */ final class NalUnitTargetBuffer {
+
+ private final int targetType;
+
+ private boolean isFilling;
+ private boolean isCompleted;
+
+ public byte[] nalData;
+ public int nalLength;
+
+ public NalUnitTargetBuffer(int targetType, int initialCapacity) {
+ this.targetType = targetType;
+
+ // Initialize data with a start code in the first three bytes.
+ nalData = new byte[3 + initialCapacity];
+ nalData[2] = 1;
+ }
+
+ /**
+ * Resets the buffer, clearing any data that it holds.
+ */
+ public void reset() {
+ isFilling = false;
+ isCompleted = false;
+ }
+
+ /**
+ * Returns whether the buffer currently holds a complete NAL unit of the target type.
+ */
+ public boolean isCompleted() {
+ return isCompleted;
+ }
+
+ /**
+ * Called to indicate that a NAL unit has started.
+ *
+ * @param type The type of the NAL unit.
+ */
+ public void startNalUnit(int type) {
+ Assertions.checkState(!isFilling);
+ isFilling = type == targetType;
+ if (isFilling) {
+ // Skip the three byte start code when writing data.
+ nalLength = 3;
+ isCompleted = false;
+ }
+ }
+
+ /**
+ * Called to pass stream data. The data passed should not include the 3 byte start code.
+ *
+ * @param data Holds the data being passed.
+ * @param offset The offset of the data in {@code data}.
+ * @param limit The limit (exclusive) of the data in {@code data}.
+ */
+ public void appendToNalUnit(byte[] data, int offset, int limit) {
+ if (!isFilling) {
+ return;
+ }
+ int readLength = limit - offset;
+ if (nalData.length < nalLength + readLength) {
+ nalData = Arrays.copyOf(nalData, (nalLength + readLength) * 2);
+ }
+ System.arraycopy(data, offset, nalData, nalLength, readLength);
+ nalLength += readLength;
+ }
+
+ /**
+ * Called to indicate that a NAL unit has ended.
+ *
+ * @param discardPadding The number of excess bytes that were passed to
+ * {@link #appendToNalUnit(byte[], int, int)}, which should be discarded.
+ * @return Whether the ended NAL unit is of the target type.
+ */
+ public boolean endNalUnit(int discardPadding) {
+ if (!isFilling) {
+ return false;
+ }
+ nalLength -= discardPadding;
+ isFilling = false;
+ isCompleted = true;
+ return true;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java
new file mode 100644
index 0000000000..86afe22563
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java
@@ -0,0 +1,241 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Parses PES packet data and extracts samples.
+ */
+public final class PesReader implements TsPayloadReader {
+
+ private static final String TAG = "PesReader";
+
+ private static final int STATE_FINDING_HEADER = 0;
+ private static final int STATE_READING_HEADER = 1;
+ private static final int STATE_READING_HEADER_EXTENSION = 2;
+ private static final int STATE_READING_BODY = 3;
+
+ private static final int HEADER_SIZE = 9;
+ private static final int MAX_HEADER_EXTENSION_SIZE = 10;
+ private static final int PES_SCRATCH_SIZE = 10; // max(HEADER_SIZE, MAX_HEADER_EXTENSION_SIZE)
+
+ private final ElementaryStreamReader reader;
+ private final ParsableBitArray pesScratch;
+
+ private int state;
+ private int bytesRead;
+
+ private TimestampAdjuster timestampAdjuster;
+ private boolean ptsFlag;
+ private boolean dtsFlag;
+ private boolean seenFirstDts;
+ private int extendedHeaderLength;
+ private int payloadSize;
+ private boolean dataAlignmentIndicator;
+ private long timeUs;
+
+ public PesReader(ElementaryStreamReader reader) {
+ this.reader = reader;
+ pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]);
+ state = STATE_FINDING_HEADER;
+ }
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator) {
+ this.timestampAdjuster = timestampAdjuster;
+ reader.createTracks(extractorOutput, idGenerator);
+ }
+
+ // TsPayloadReader implementation.
+
+ @Override
+ public final void seek() {
+ state = STATE_FINDING_HEADER;
+ bytesRead = 0;
+ seenFirstDts = false;
+ reader.seek();
+ }
+
+ @Override
+ public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException {
+ if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) {
+ switch (state) {
+ case STATE_FINDING_HEADER:
+ case STATE_READING_HEADER:
+ // Expected.
+ break;
+ case STATE_READING_HEADER_EXTENSION:
+ Log.w(TAG, "Unexpected start indicator reading extended header");
+ break;
+ case STATE_READING_BODY:
+ // If payloadSize == -1 then the length of the previous packet was unspecified, and so
+ // we only know that it's finished now that we've seen the start of the next one. This
+ // is expected. If payloadSize != -1, then the length of the previous packet was known,
+ // but we didn't receive that amount of data. This is not expected.
+ if (payloadSize != -1) {
+ Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes");
+ }
+ // Either way, notify the reader that it has now finished.
+ reader.packetFinished();
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ setState(STATE_READING_HEADER);
+ }
+
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_HEADER:
+ data.skipBytes(data.bytesLeft());
+ break;
+ case STATE_READING_HEADER:
+ if (continueRead(data, pesScratch.data, HEADER_SIZE)) {
+ setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER);
+ }
+ break;
+ case STATE_READING_HEADER_EXTENSION:
+ int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength);
+ // Read as much of the extended header as we're interested in, and skip the rest.
+ if (continueRead(data, pesScratch.data, readLength)
+ && continueRead(data, null, extendedHeaderLength)) {
+ parseHeaderExtension();
+ flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;
+ reader.packetStarted(timeUs, flags);
+ setState(STATE_READING_BODY);
+ }
+ break;
+ case STATE_READING_BODY:
+ readLength = data.bytesLeft();
+ int padding = payloadSize == -1 ? 0 : readLength - payloadSize;
+ if (padding > 0) {
+ readLength -= padding;
+ data.setLimit(data.getPosition() + readLength);
+ }
+ reader.consume(data);
+ if (payloadSize != -1) {
+ payloadSize -= readLength;
+ if (payloadSize == 0) {
+ reader.packetFinished();
+ setState(STATE_READING_HEADER);
+ }
+ }
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ private void setState(int state) {
+ this.state = state;
+ bytesRead = 0;
+ }
+
+ /**
+ * Continues a read from the provided {@code source} into a given {@code target}. It's assumed
+ * that the data should be written into {@code target} starting from an offset of zero.
+ *
+ * @param source The source from which to read.
+ * @param target The target into which data is to be read, or {@code null} to skip.
+ * @param targetLength The target length of the read.
+ * @return Whether the target length has been reached.
+ */
+ private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
+ int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
+ if (bytesToRead <= 0) {
+ return true;
+ } else if (target == null) {
+ source.skipBytes(bytesToRead);
+ } else {
+ source.readBytes(target, bytesRead, bytesToRead);
+ }
+ bytesRead += bytesToRead;
+ return bytesRead == targetLength;
+ }
+
+ private boolean parseHeader() {
+ // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of
+ // the header.
+ pesScratch.setPosition(0);
+ int startCodePrefix = pesScratch.readBits(24);
+ if (startCodePrefix != 0x000001) {
+ Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix);
+ payloadSize = -1;
+ return false;
+ }
+
+ pesScratch.skipBits(8); // stream_id.
+ int packetLength = pesScratch.readBits(16);
+ pesScratch.skipBits(5); // '10' (2), PES_scrambling_control (2), PES_priority (1)
+ dataAlignmentIndicator = pesScratch.readBit();
+ pesScratch.skipBits(2); // copyright (1), original_or_copy (1)
+ ptsFlag = pesScratch.readBit();
+ dtsFlag = pesScratch.readBit();
+ // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1),
+ // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1)
+ pesScratch.skipBits(6);
+ extendedHeaderLength = pesScratch.readBits(8);
+
+ if (packetLength == 0) {
+ payloadSize = -1;
+ } else {
+ payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */
+ - HEADER_SIZE - extendedHeaderLength;
+ }
+ return true;
+ }
+
+ private void parseHeaderExtension() {
+ pesScratch.setPosition(0);
+ timeUs = C.TIME_UNSET;
+ if (ptsFlag) {
+ pesScratch.skipBits(4); // '0010' or '0011'
+ long pts = (long) pesScratch.readBits(3) << 30;
+ pesScratch.skipBits(1); // marker_bit
+ pts |= pesScratch.readBits(15) << 15;
+ pesScratch.skipBits(1); // marker_bit
+ pts |= pesScratch.readBits(15);
+ pesScratch.skipBits(1); // marker_bit
+ if (!seenFirstDts && dtsFlag) {
+ pesScratch.skipBits(4); // '0011'
+ long dts = (long) pesScratch.readBits(3) << 30;
+ pesScratch.skipBits(1); // marker_bit
+ dts |= pesScratch.readBits(15) << 15;
+ pesScratch.skipBits(1); // marker_bit
+ dts |= pesScratch.readBits(15);
+ pesScratch.skipBits(1); // marker_bit
+ // Subsequent PES packets may have earlier presentation timestamps than this one, but they
+ // should all be greater than or equal to this packet's decode timestamp. We feed the
+ // decode timestamp to the adjuster here so that in the case that this is the first to be
+ // fed, the adjuster will be able to compute an offset to apply such that the adjusted
+ // presentation timestamps of all future packets are non-negative.
+ timestampAdjuster.adjustTsTimestamp(dts);
+ seenFirstDts = true;
+ }
+ timeUs = timestampAdjuster.adjustTsTimestamp(pts);
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java
new file mode 100644
index 0000000000..acd08a2f12
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.BinarySearchSeeker;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A seeker that supports seeking within PS stream using binary search.
+ *
+ * <p>This seeker uses the first and last SCR values within the stream, as well as the stream
+ * duration to interpolate the SCR value of the seeking position. Then it performs binary search
+ * within the stream to find a packets whose SCR value is with in {@link #SEEK_TOLERANCE_US} from
+ * the target SCR.
+ */
+/* package */ final class PsBinarySearchSeeker extends BinarySearchSeeker {
+
+ private static final long SEEK_TOLERANCE_US = 100_000;
+ private static final int MINIMUM_SEARCH_RANGE_BYTES = 1000;
+ private static final int TIMESTAMP_SEARCH_BYTES = 20000;
+
+ public PsBinarySearchSeeker(
+ TimestampAdjuster scrTimestampAdjuster, long streamDurationUs, long inputLength) {
+ super(
+ new DefaultSeekTimestampConverter(),
+ new PsScrSeeker(scrTimestampAdjuster),
+ streamDurationUs,
+ /* floorTimePosition= */ 0,
+ /* ceilingTimePosition= */ streamDurationUs + 1,
+ /* floorBytePosition= */ 0,
+ /* ceilingBytePosition= */ inputLength,
+ /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE,
+ MINIMUM_SEARCH_RANGE_BYTES);
+ }
+
+ /**
+ * A seeker that looks for a given SCR timestamp at a given position in a PS stream.
+ *
+ * <p>Given a SCR timestamp, and a position within a PS stream, this seeker will peek up to {@link
+ * #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all packs in that range, and
+ * then compare the SCR timestamps (if available) of these packets to the target timestamp.
+ */
+ private static final class PsScrSeeker implements TimestampSeeker {
+
+ private final TimestampAdjuster scrTimestampAdjuster;
+ private final ParsableByteArray packetBuffer;
+
+ private PsScrSeeker(TimestampAdjuster scrTimestampAdjuster) {
+ this.scrTimestampAdjuster = scrTimestampAdjuster;
+ packetBuffer = new ParsableByteArray();
+ }
+
+ @Override
+ public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
+ throws IOException, InterruptedException {
+ long inputPosition = input.getPosition();
+ int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition);
+
+ packetBuffer.reset(bytesToSearch);
+ input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
+
+ return searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
+ }
+
+ @Override
+ public void onSeekFinished() {
+ packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
+ }
+
+ private TimestampSearchResult searchForScrValueInBuffer(
+ ParsableByteArray packetBuffer, long targetScrTimeUs, long bufferStartOffset) {
+ int startOfLastPacketPosition = C.POSITION_UNSET;
+ int endOfLastPacketPosition = C.POSITION_UNSET;
+ long lastScrTimeUsInRange = C.TIME_UNSET;
+
+ while (packetBuffer.bytesLeft() >= 4) {
+ int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());
+ if (nextStartCode != PsExtractor.PACK_START_CODE) {
+ packetBuffer.skipBytes(1);
+ continue;
+ } else {
+ packetBuffer.skipBytes(4);
+ }
+
+ // We found a pack.
+ long scrValue = PsDurationReader.readScrValueFromPack(packetBuffer);
+ if (scrValue != C.TIME_UNSET) {
+ long scrTimeUs = scrTimestampAdjuster.adjustTsTimestamp(scrValue);
+ if (scrTimeUs > targetScrTimeUs) {
+ if (lastScrTimeUsInRange == C.TIME_UNSET) {
+ // First SCR timestamp is already over target.
+ return TimestampSearchResult.overestimatedResult(scrTimeUs, bufferStartOffset);
+ } else {
+ // Last SCR timestamp < target timestamp < this timestamp.
+ return TimestampSearchResult.targetFoundResult(
+ bufferStartOffset + startOfLastPacketPosition);
+ }
+ } else if (scrTimeUs + SEEK_TOLERANCE_US > targetScrTimeUs) {
+ long startOfPacketInStream = bufferStartOffset + packetBuffer.getPosition();
+ return TimestampSearchResult.targetFoundResult(startOfPacketInStream);
+ }
+
+ lastScrTimeUsInRange = scrTimeUs;
+ startOfLastPacketPosition = packetBuffer.getPosition();
+ }
+ skipToEndOfCurrentPack(packetBuffer);
+ endOfLastPacketPosition = packetBuffer.getPosition();
+ }
+
+ if (lastScrTimeUsInRange != C.TIME_UNSET) {
+ long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition;
+ return TimestampSearchResult.underestimatedResult(
+ lastScrTimeUsInRange, endOfLastPacketPositionInStream);
+ } else {
+ return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
+ }
+ }
+
+ /**
+ * Skips the buffer position to the position after the end of the current PS pack in the buffer,
+ * given the byte position right after the {@link PsExtractor#PACK_START_CODE} of the pack in
+ * the buffer. If the pack ends after the end of the buffer, skips to the end of the buffer.
+ */
+ private static void skipToEndOfCurrentPack(ParsableByteArray packetBuffer) {
+ int limit = packetBuffer.limit();
+
+ if (packetBuffer.bytesLeft() < 10) {
+ // We require at least 9 bytes for pack header to read SCR value + 1 byte for pack_stuffing
+ // length.
+ packetBuffer.setPosition(limit);
+ return;
+ }
+ packetBuffer.skipBytes(9);
+
+ int packStuffingLength = packetBuffer.readUnsignedByte() & 0x07;
+ if (packetBuffer.bytesLeft() < packStuffingLength) {
+ packetBuffer.setPosition(limit);
+ return;
+ }
+ packetBuffer.skipBytes(packStuffingLength);
+
+ if (packetBuffer.bytesLeft() < 4) {
+ packetBuffer.setPosition(limit);
+ return;
+ }
+
+ int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());
+ if (nextStartCode == PsExtractor.SYSTEM_HEADER_START_CODE) {
+ packetBuffer.skipBytes(4);
+ int systemHeaderLength = packetBuffer.readUnsignedShort();
+ if (packetBuffer.bytesLeft() < systemHeaderLength) {
+ packetBuffer.setPosition(limit);
+ return;
+ }
+ packetBuffer.skipBytes(systemHeaderLength);
+ }
+
+ // Find the position of the next PACK_START_CODE or MPEG_PROGRAM_END_CODE, which is right
+ // after the end position of this pack.
+ // If we couldn't find these codes within the buffer, return the buffer limit, or return
+ // the first position which PES packets pattern does not match (some malformed packets).
+ while (packetBuffer.bytesLeft() >= 4) {
+ nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition());
+ if (nextStartCode == PsExtractor.PACK_START_CODE
+ || nextStartCode == PsExtractor.MPEG_PROGRAM_END_CODE) {
+ break;
+ }
+ if (nextStartCode >>> 8 != PsExtractor.PACKET_START_CODE_PREFIX) {
+ break;
+ }
+ packetBuffer.skipBytes(4);
+
+ if (packetBuffer.bytesLeft() < 2) {
+ // 2 bytes for PES_packet length.
+ packetBuffer.setPosition(limit);
+ return;
+ }
+ int pesPacketLength = packetBuffer.readUnsignedShort();
+ packetBuffer.setPosition(
+ Math.min(packetBuffer.limit(), packetBuffer.getPosition() + pesPacketLength));
+ }
+ }
+ }
+
+ private static int peekIntAtPosition(byte[] data, int position) {
+ return (data[position] & 0xFF) << 24
+ | (data[position + 1] & 0xFF) << 16
+ | (data[position + 2] & 0xFF) << 8
+ | (data[position + 3] & 0xFF);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java
new file mode 100644
index 0000000000..a5960fbe15
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A reader that can extract the approximate duration from a given MPEG program stream (PS).
+ *
+ * <p>This reader extracts the duration by reading system clock reference (SCR) values from the
+ * header of a pack at the start and at the end of the stream, calculating the difference, and
+ * converting that into stream duration. This reader also handles the case when a single SCR
+ * wraparound takes place within the stream, which can make SCR values at the beginning of the
+ * stream larger than SCR values at the end. This class can only be used once to read duration from
+ * a given stream, and the usage of the class is not thread-safe, so all calls should be made from
+ * the same thread.
+ *
+ * <p>Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in pack_header.
+ */
+/* package */ final class PsDurationReader {
+
+ private static final int TIMESTAMP_SEARCH_BYTES = 20000;
+
+ private final TimestampAdjuster scrTimestampAdjuster;
+ private final ParsableByteArray packetBuffer;
+
+ private boolean isDurationRead;
+ private boolean isFirstScrValueRead;
+ private boolean isLastScrValueRead;
+
+ private long firstScrValue;
+ private long lastScrValue;
+ private long durationUs;
+
+ /* package */ PsDurationReader() {
+ scrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);
+ firstScrValue = C.TIME_UNSET;
+ lastScrValue = C.TIME_UNSET;
+ durationUs = C.TIME_UNSET;
+ packetBuffer = new ParsableByteArray();
+ }
+
+ /** Returns true if a PS duration has been read. */
+ public boolean isDurationReadFinished() {
+ return isDurationRead;
+ }
+
+ public TimestampAdjuster getScrTimestampAdjuster() {
+ return scrTimestampAdjuster;
+ }
+
+ /**
+ * Reads a PS duration from the input.
+ *
+ * <p>This reader reads the duration by reading SCR values from the header of a pack at the start
+ * and at the end of the stream, calculating the difference, and converting that into stream
+ * duration.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
+ * to hold the position of the required seek.
+ * @return One of the {@code RESULT_} values defined in {@link Extractor}.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public @Extractor.ReadResult int readDuration(
+ ExtractorInput input, PositionHolder seekPositionHolder)
+ throws IOException, InterruptedException {
+ if (!isLastScrValueRead) {
+ return readLastScrValue(input, seekPositionHolder);
+ }
+ if (lastScrValue == C.TIME_UNSET) {
+ return finishReadDuration(input);
+ }
+ if (!isFirstScrValueRead) {
+ return readFirstScrValue(input, seekPositionHolder);
+ }
+ if (firstScrValue == C.TIME_UNSET) {
+ return finishReadDuration(input);
+ }
+
+ long minScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(firstScrValue);
+ long maxScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(lastScrValue);
+ durationUs = maxScrPositionUs - minScrPositionUs;
+ return finishReadDuration(input);
+ }
+
+ /** Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder)}. */
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /**
+ * Returns the SCR value read from the next pack in the stream, given the buffer at the pack
+ * header start position (just behind the pack start code).
+ */
+ public static long readScrValueFromPack(ParsableByteArray packetBuffer) {
+ int originalPosition = packetBuffer.getPosition();
+ if (packetBuffer.bytesLeft() < 9) {
+ // We require at 9 bytes for pack header to read scr value
+ return C.TIME_UNSET;
+ }
+ byte[] scrBytes = new byte[9];
+ packetBuffer.readBytes(scrBytes, /* offset= */ 0, scrBytes.length);
+ packetBuffer.setPosition(originalPosition);
+ if (!checkMarkerBits(scrBytes)) {
+ return C.TIME_UNSET;
+ }
+ return readScrValueFromPackHeader(scrBytes);
+ }
+
+ private int finishReadDuration(ExtractorInput input) {
+ packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
+ isDurationRead = true;
+ input.resetPeekPosition();
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private int readFirstScrValue(ExtractorInput input, PositionHolder seekPositionHolder)
+ throws IOException, InterruptedException {
+ int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength());
+ int searchStartPosition = 0;
+ if (input.getPosition() != searchStartPosition) {
+ seekPositionHolder.position = searchStartPosition;
+ return Extractor.RESULT_SEEK;
+ }
+
+ packetBuffer.reset(bytesToSearch);
+ input.resetPeekPosition();
+ input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
+
+ firstScrValue = readFirstScrValueFromBuffer(packetBuffer);
+ isFirstScrValueRead = true;
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private long readFirstScrValueFromBuffer(ParsableByteArray packetBuffer) {
+ int searchStartPosition = packetBuffer.getPosition();
+ int searchEndPosition = packetBuffer.limit();
+ for (int searchPosition = searchStartPosition;
+ searchPosition < searchEndPosition - 3;
+ searchPosition++) {
+ int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition);
+ if (nextStartCode == PsExtractor.PACK_START_CODE) {
+ packetBuffer.setPosition(searchPosition + 4);
+ long scrValue = readScrValueFromPack(packetBuffer);
+ if (scrValue != C.TIME_UNSET) {
+ return scrValue;
+ }
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+ private int readLastScrValue(ExtractorInput input, PositionHolder seekPositionHolder)
+ throws IOException, InterruptedException {
+ long inputLength = input.getLength();
+ int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength);
+ long searchStartPosition = inputLength - bytesToSearch;
+ if (input.getPosition() != searchStartPosition) {
+ seekPositionHolder.position = searchStartPosition;
+ return Extractor.RESULT_SEEK;
+ }
+
+ packetBuffer.reset(bytesToSearch);
+ input.resetPeekPosition();
+ input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
+
+ lastScrValue = readLastScrValueFromBuffer(packetBuffer);
+ isLastScrValueRead = true;
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private long readLastScrValueFromBuffer(ParsableByteArray packetBuffer) {
+ int searchStartPosition = packetBuffer.getPosition();
+ int searchEndPosition = packetBuffer.limit();
+ for (int searchPosition = searchEndPosition - 4;
+ searchPosition >= searchStartPosition;
+ searchPosition--) {
+ int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition);
+ if (nextStartCode == PsExtractor.PACK_START_CODE) {
+ packetBuffer.setPosition(searchPosition + 4);
+ long scrValue = readScrValueFromPack(packetBuffer);
+ if (scrValue != C.TIME_UNSET) {
+ return scrValue;
+ }
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+ private int peekIntAtPosition(byte[] data, int position) {
+ return (data[position] & 0xFF) << 24
+ | (data[position + 1] & 0xFF) << 16
+ | (data[position + 2] & 0xFF) << 8
+ | (data[position + 3] & 0xFF);
+ }
+
+ private static boolean checkMarkerBits(byte[] scrBytes) {
+ // Verify the 01xxx1xx marker on the 0th byte
+ if ((scrBytes[0] & 0xC4) != 0x44) {
+ return false;
+ }
+ // 1st byte belongs to scr field.
+ // Verify the xxxxx1xx marker on the 2nd byte
+ if ((scrBytes[2] & 0x04) != 0x04) {
+ return false;
+ }
+ // 3rd byte belongs to scr field.
+ // Verify the xxxxx1xx marker on the 4rd byte
+ if ((scrBytes[4] & 0x04) != 0x04) {
+ return false;
+ }
+ // Verify the xxxxxxx1 marker on the 5th byte
+ if ((scrBytes[5] & 0x01) != 0x01) {
+ return false;
+ }
+ // 6th and 7th bytes belongs to program_max_rate field.
+ // Verify the xxxxxx11 marker on the 8th byte
+ return (scrBytes[8] & 0x03) == 0x03;
+ }
+
+ /**
+ * Returns the value of SCR base - 33 bits in big endian order from the PS pack header, ignoring
+ * the marker bits. Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in
+ * pack_header.
+ *
+ * <p>We ignore SCR Ext, because it's too small to have any significance.
+ */
+ private static long readScrValueFromPackHeader(byte[] scrBytes) {
+ return ((scrBytes[0] & 0b00111000L) >> 3) << 30
+ | (scrBytes[0] & 0b00000011L) << 28
+ | (scrBytes[1] & 0xFFL) << 20
+ | ((scrBytes[2] & 0b11111000L) >> 3) << 15
+ | (scrBytes[2] & 0b00000011L) << 13
+ | (scrBytes[3] & 0xFFL) << 5
+ | (scrBytes[4] & 0b11111000L) >> 3;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
new file mode 100644
index 0000000000..8dcccbe459
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
@@ -0,0 +1,397 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import android.util.SparseArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.IOException;
+
+/**
+ * Extracts data from the MPEG-2 PS container format.
+ */
+public final class PsExtractor implements Extractor {
+
+ /** Factory for {@link PsExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new PsExtractor()};
+
+ /* package */ static final int PACK_START_CODE = 0x000001BA;
+ /* package */ static final int SYSTEM_HEADER_START_CODE = 0x000001BB;
+ /* package */ static final int PACKET_START_CODE_PREFIX = 0x000001;
+ /* package */ static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
+ private static final int MAX_STREAM_ID_PLUS_ONE = 0x100;
+
+ // Max search length for first audio and video track in input data.
+ private static final long MAX_SEARCH_LENGTH = 1024 * 1024;
+ // Max search length for additional audio and video tracks in input data after at least one audio
+ // and video track has been found.
+ private static final long MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND = 8 * 1024;
+
+ public static final int PRIVATE_STREAM_1 = 0xBD;
+ public static final int AUDIO_STREAM = 0xC0;
+ public static final int AUDIO_STREAM_MASK = 0xE0;
+ public static final int VIDEO_STREAM = 0xE0;
+ public static final int VIDEO_STREAM_MASK = 0xF0;
+
+ private final TimestampAdjuster timestampAdjuster;
+ private final SparseArray<PesReader> psPayloadReaders; // Indexed by pid
+ private final ParsableByteArray psPacketBuffer;
+ private final PsDurationReader durationReader;
+
+ private boolean foundAllTracks;
+ private boolean foundAudioTrack;
+ private boolean foundVideoTrack;
+ private long lastTrackPosition;
+
+ // Accessed only by the loading thread.
+ private PsBinarySearchSeeker psBinarySearchSeeker;
+ private ExtractorOutput output;
+ private boolean hasOutputSeekMap;
+
+ public PsExtractor() {
+ this(new TimestampAdjuster(0));
+ }
+
+ public PsExtractor(TimestampAdjuster timestampAdjuster) {
+ this.timestampAdjuster = timestampAdjuster;
+ psPacketBuffer = new ParsableByteArray(4096);
+ psPayloadReaders = new SparseArray<>();
+ durationReader = new PsDurationReader();
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ byte[] scratch = new byte[14];
+ input.peekFully(scratch, 0, 14);
+
+ // Verify the PACK_START_CODE for the first 4 bytes
+ if (PACK_START_CODE != (((scratch[0] & 0xFF) << 24) | ((scratch[1] & 0xFF) << 16)
+ | ((scratch[2] & 0xFF) << 8) | (scratch[3] & 0xFF))) {
+ return false;
+ }
+ // Verify the 01xxx1xx marker on the 5th byte
+ if ((scratch[4] & 0xC4) != 0x44) {
+ return false;
+ }
+ // Verify the xxxxx1xx marker on the 7th byte
+ if ((scratch[6] & 0x04) != 0x04) {
+ return false;
+ }
+ // Verify the xxxxx1xx marker on the 9th byte
+ if ((scratch[8] & 0x04) != 0x04) {
+ return false;
+ }
+ // Verify the xxxxxxx1 marker on the 10th byte
+ if ((scratch[9] & 0x01) != 0x01) {
+ return false;
+ }
+ // Verify the xxxxxx11 marker on the 13th byte
+ if ((scratch[12] & 0x03) != 0x03) {
+ return false;
+ }
+ // Read the stuffing length from the 14th byte (last 3 bits)
+ int packStuffingLength = scratch[13] & 0x07;
+ input.advancePeekPosition(packStuffingLength);
+ // Now check that the next 3 bytes are the beginning of an MPEG start code
+ input.peekFully(scratch, 0, 3);
+ return (PACKET_START_CODE_PREFIX == (((scratch[0] & 0xFF) << 16) | ((scratch[1] & 0xFF) << 8)
+ | (scratch[2] & 0xFF)));
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.output = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ boolean hasNotEncounteredFirstTimestamp =
+ timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET;
+ if (hasNotEncounteredFirstTimestamp
+ || (timestampAdjuster.getFirstSampleTimestampUs() != 0
+ && timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) {
+ // - If the timestamp adjuster in the PS stream has not encountered any sample, it's going to
+ // treat the first timestamp encountered as sample time 0, which is incorrect. In this case,
+ // we have to set the first sample timestamp manually.
+ // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a
+ // different position, we need to set the first sample timestamp manually again.
+ timestampAdjuster.reset();
+ timestampAdjuster.setFirstSampleTimestampUs(timeUs);
+ }
+
+ if (psBinarySearchSeeker != null) {
+ psBinarySearchSeeker.setSeekTargetUs(timeUs);
+ }
+ for (int i = 0; i < psPayloadReaders.size(); i++) {
+ psPayloadReaders.valueAt(i).seek();
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+
+ long inputLength = input.getLength();
+ boolean canReadDuration = inputLength != C.LENGTH_UNSET;
+ if (canReadDuration && !durationReader.isDurationReadFinished()) {
+ return durationReader.readDuration(input, seekPosition);
+ }
+ maybeOutputSeekMap(inputLength);
+ if (psBinarySearchSeeker != null && psBinarySearchSeeker.isSeeking()) {
+ return psBinarySearchSeeker.handlePendingSeek(input, seekPosition);
+ }
+
+ input.resetPeekPosition();
+ long peekBytesLeft =
+ inputLength != C.LENGTH_UNSET ? inputLength - input.getPeekPosition() : C.LENGTH_UNSET;
+ if (peekBytesLeft != C.LENGTH_UNSET && peekBytesLeft < 4) {
+ return RESULT_END_OF_INPUT;
+ }
+ // First peek and check what type of start code is next.
+ if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ psPacketBuffer.setPosition(0);
+ int nextStartCode = psPacketBuffer.readInt();
+ if (nextStartCode == MPEG_PROGRAM_END_CODE) {
+ return RESULT_END_OF_INPUT;
+ } else if (nextStartCode == PACK_START_CODE) {
+ // Now peek the rest of the pack_header.
+ input.peekFully(psPacketBuffer.data, 0, 10);
+
+ // We only care about the pack_stuffing_length in here, skip the first 77 bits.
+ psPacketBuffer.setPosition(9);
+
+ // Last 3 bits is the length.
+ int packStuffingLength = psPacketBuffer.readUnsignedByte() & 0x07;
+
+ // Now skip the stuffing and the pack header.
+ input.skipFully(packStuffingLength + 14);
+ return RESULT_CONTINUE;
+ } else if (nextStartCode == SYSTEM_HEADER_START_CODE) {
+ // We just skip all this, but we need to get the length first.
+ input.peekFully(psPacketBuffer.data, 0, 2);
+
+ // Length is the next 2 bytes.
+ psPacketBuffer.setPosition(0);
+ int systemHeaderLength = psPacketBuffer.readUnsignedShort();
+ input.skipFully(systemHeaderLength + 6);
+ return RESULT_CONTINUE;
+ } else if (((nextStartCode & 0xFFFFFF00) >> 8) != PACKET_START_CODE_PREFIX) {
+ input.skipFully(1); // Skip bytes until we see a valid start code again.
+ return RESULT_CONTINUE;
+ }
+
+ // We're at the start of a regular PES packet now.
+ // Get the stream ID off the last byte of the start code.
+ int streamId = nextStartCode & 0xFF;
+
+ // Check to see if we have this one in our map yet, and if not, then add it.
+ PesReader payloadReader = psPayloadReaders.get(streamId);
+ if (!foundAllTracks) {
+ if (payloadReader == null) {
+ ElementaryStreamReader elementaryStreamReader = null;
+ if (streamId == PRIVATE_STREAM_1) {
+ // Private stream, used for AC3 audio.
+ // NOTE: This may need further parsing to determine if its DTS, but that's likely only
+ // valid for DVDs.
+ elementaryStreamReader = new Ac3Reader();
+ foundAudioTrack = true;
+ lastTrackPosition = input.getPosition();
+ } else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
+ elementaryStreamReader = new MpegAudioReader();
+ foundAudioTrack = true;
+ lastTrackPosition = input.getPosition();
+ } else if ((streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
+ elementaryStreamReader = new H262Reader();
+ foundVideoTrack = true;
+ lastTrackPosition = input.getPosition();
+ }
+ if (elementaryStreamReader != null) {
+ TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE);
+ elementaryStreamReader.createTracks(output, idGenerator);
+ payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster);
+ psPayloadReaders.put(streamId, payloadReader);
+ }
+ }
+ long maxSearchPosition =
+ foundAudioTrack && foundVideoTrack
+ ? lastTrackPosition + MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND
+ : MAX_SEARCH_LENGTH;
+ if (input.getPosition() > maxSearchPosition) {
+ foundAllTracks = true;
+ output.endTracks();
+ }
+ }
+
+ // The next 2 bytes are the length. Once we have that we can consume the complete packet.
+ input.peekFully(psPacketBuffer.data, 0, 2);
+ psPacketBuffer.setPosition(0);
+ int payloadLength = psPacketBuffer.readUnsignedShort();
+ int pesLength = payloadLength + 6;
+
+ if (payloadReader == null) {
+ // Just skip this data.
+ input.skipFully(pesLength);
+ } else {
+ psPacketBuffer.reset(pesLength);
+ // Read the whole packet and the header for consumption.
+ input.readFully(psPacketBuffer.data, 0, pesLength);
+ psPacketBuffer.setPosition(6);
+ payloadReader.consume(psPacketBuffer);
+ psPacketBuffer.setLimit(psPacketBuffer.capacity());
+ }
+
+ return RESULT_CONTINUE;
+ }
+
+ // Internals.
+
+ private void maybeOutputSeekMap(long inputLength) {
+ if (!hasOutputSeekMap) {
+ hasOutputSeekMap = true;
+ if (durationReader.getDurationUs() != C.TIME_UNSET) {
+ psBinarySearchSeeker =
+ new PsBinarySearchSeeker(
+ durationReader.getScrTimestampAdjuster(),
+ durationReader.getDurationUs(),
+ inputLength);
+ output.seekMap(psBinarySearchSeeker.getSeekMap());
+ } else {
+ output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));
+ }
+ }
+ }
+
+ /**
+ * Parses PES packet data and extracts samples.
+ */
+ private static final class PesReader {
+
+ private static final int PES_SCRATCH_SIZE = 64;
+
+ private final ElementaryStreamReader pesPayloadReader;
+ private final TimestampAdjuster timestampAdjuster;
+ private final ParsableBitArray pesScratch;
+
+ private boolean ptsFlag;
+ private boolean dtsFlag;
+ private boolean seenFirstDts;
+ private int extendedHeaderLength;
+ private long timeUs;
+
+ public PesReader(ElementaryStreamReader pesPayloadReader, TimestampAdjuster timestampAdjuster) {
+ this.pesPayloadReader = pesPayloadReader;
+ this.timestampAdjuster = timestampAdjuster;
+ pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]);
+ }
+
+ /**
+ * Notifies the reader that a seek has occurred.
+ * <p>
+ * Following a call to this method, the data passed to the next invocation of
+ * {@link #consume(ParsableByteArray)} will not be a continuation of the data that was
+ * previously passed. Hence the reader should reset any internal state.
+ */
+ public void seek() {
+ seenFirstDts = false;
+ pesPayloadReader.seek();
+ }
+
+ /**
+ * Consumes the payload of a PS packet.
+ *
+ * @param data The PES packet. The position will be set to the start of the payload.
+ * @throws ParserException If the payload could not be parsed.
+ */
+ public void consume(ParsableByteArray data) throws ParserException {
+ data.readBytes(pesScratch.data, 0, 3);
+ pesScratch.setPosition(0);
+ parseHeader();
+ data.readBytes(pesScratch.data, 0, extendedHeaderLength);
+ pesScratch.setPosition(0);
+ parseHeaderExtension();
+ pesPayloadReader.packetStarted(timeUs, TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR);
+ pesPayloadReader.consume(data);
+ // We always have complete PES packets with program stream.
+ pesPayloadReader.packetFinished();
+ }
+
+ private void parseHeader() {
+ // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of
+ // the header.
+ // First 8 bits are skipped: '10' (2), PES_scrambling_control (2), PES_priority (1),
+ // data_alignment_indicator (1), copyright (1), original_or_copy (1)
+ pesScratch.skipBits(8);
+ ptsFlag = pesScratch.readBit();
+ dtsFlag = pesScratch.readBit();
+ // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1),
+ // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1)
+ pesScratch.skipBits(6);
+ extendedHeaderLength = pesScratch.readBits(8);
+ }
+
+ private void parseHeaderExtension() {
+ timeUs = 0;
+ if (ptsFlag) {
+ pesScratch.skipBits(4); // '0010' or '0011'
+ long pts = (long) pesScratch.readBits(3) << 30;
+ pesScratch.skipBits(1); // marker_bit
+ pts |= pesScratch.readBits(15) << 15;
+ pesScratch.skipBits(1); // marker_bit
+ pts |= pesScratch.readBits(15);
+ pesScratch.skipBits(1); // marker_bit
+ if (!seenFirstDts && dtsFlag) {
+ pesScratch.skipBits(4); // '0011'
+ long dts = (long) pesScratch.readBits(3) << 30;
+ pesScratch.skipBits(1); // marker_bit
+ dts |= pesScratch.readBits(15) << 15;
+ pesScratch.skipBits(1); // marker_bit
+ dts |= pesScratch.readBits(15);
+ pesScratch.skipBits(1); // marker_bit
+ // Subsequent PES packets may have earlier presentation timestamps than this one, but they
+ // should all be greater than or equal to this packet's decode timestamp. We feed the
+ // decode timestamp to the adjuster here so that in the case that this is the first to be
+ // fed, the adjuster will be able to compute an offset to apply such that the adjusted
+ // presentation timestamps of all future packets are non-negative.
+ timestampAdjuster.adjustTsTimestamp(dts);
+ seenFirstDts = true;
+ }
+ timeUs = timestampAdjuster.adjustTsTimestamp(pts);
+ }
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java
new file mode 100644
index 0000000000..b5942b8bcc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java
@@ -0,0 +1,49 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Reads section data.
+ */
+public interface SectionPayloadReader {
+
+ /**
+ * Initializes the section payload reader.
+ *
+ * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
+ * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.
+ * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the
+ * {@link TrackOutput}s.
+ */
+ void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator);
+
+ /**
+ * Called by a {@link SectionReader} when a full section is received.
+ *
+ * @param sectionData The data belonging to a section starting from the table_id. If
+ * section_syntax_indicator is set to '1', {@code sectionData} excludes the CRC_32 field.
+ * Otherwise, all bytes belonging to the table section are included.
+ */
+ void consume(ParsableByteArray sectionData);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java
new file mode 100644
index 0000000000..61b53cfa72
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java
@@ -0,0 +1,134 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/**
+ * Reads section data packets and feeds the whole sections to a given {@link SectionPayloadReader}.
+ * Useful information on PSI sections can be found in ISO/IEC 13818-1, section 2.4.4.
+ */
+public final class SectionReader implements TsPayloadReader {
+
+ private static final int SECTION_HEADER_LENGTH = 3;
+ private static final int DEFAULT_SECTION_BUFFER_LENGTH = 32;
+ private static final int MAX_SECTION_LENGTH = 4098;
+
+ private final SectionPayloadReader reader;
+ private final ParsableByteArray sectionData;
+
+ private int totalSectionLength;
+ private int bytesRead;
+ private boolean sectionSyntaxIndicator;
+ private boolean waitingForPayloadStart;
+
+ public SectionReader(SectionPayloadReader reader) {
+ this.reader = reader;
+ sectionData = new ParsableByteArray(DEFAULT_SECTION_BUFFER_LENGTH);
+ }
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator) {
+ reader.init(timestampAdjuster, extractorOutput, idGenerator);
+ waitingForPayloadStart = true;
+ }
+
+ @Override
+ public void seek() {
+ waitingForPayloadStart = true;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data, @Flags int flags) {
+ boolean payloadUnitStartIndicator = (flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0;
+ int payloadStartPosition = C.POSITION_UNSET;
+ if (payloadUnitStartIndicator) {
+ int payloadStartOffset = data.readUnsignedByte();
+ payloadStartPosition = data.getPosition() + payloadStartOffset;
+ }
+
+ if (waitingForPayloadStart) {
+ if (!payloadUnitStartIndicator) {
+ return;
+ }
+ waitingForPayloadStart = false;
+ data.setPosition(payloadStartPosition);
+ bytesRead = 0;
+ }
+
+ while (data.bytesLeft() > 0) {
+ if (bytesRead < SECTION_HEADER_LENGTH) {
+ // Note: see ISO/IEC 13818-1, section 2.4.4.3 for detailed information on the format of
+ // the header.
+ if (bytesRead == 0) {
+ int tableId = data.readUnsignedByte();
+ data.setPosition(data.getPosition() - 1);
+ if (tableId == 0xFF /* forbidden value */) {
+ // No more sections in this ts packet.
+ waitingForPayloadStart = true;
+ return;
+ }
+ }
+ int headerBytesToRead = Math.min(data.bytesLeft(), SECTION_HEADER_LENGTH - bytesRead);
+ data.readBytes(sectionData.data, bytesRead, headerBytesToRead);
+ bytesRead += headerBytesToRead;
+ if (bytesRead == SECTION_HEADER_LENGTH) {
+ sectionData.reset(SECTION_HEADER_LENGTH);
+ sectionData.skipBytes(1); // Skip table id (8).
+ int secondHeaderByte = sectionData.readUnsignedByte();
+ int thirdHeaderByte = sectionData.readUnsignedByte();
+ sectionSyntaxIndicator = (secondHeaderByte & 0x80) != 0;
+ totalSectionLength =
+ (((secondHeaderByte & 0x0F) << 8) | thirdHeaderByte) + SECTION_HEADER_LENGTH;
+ if (sectionData.capacity() < totalSectionLength) {
+ // Ensure there is enough space to keep the whole section.
+ byte[] bytes = sectionData.data;
+ sectionData.reset(
+ Math.min(MAX_SECTION_LENGTH, Math.max(totalSectionLength, bytes.length * 2)));
+ System.arraycopy(bytes, 0, sectionData.data, 0, SECTION_HEADER_LENGTH);
+ }
+ }
+ } else {
+ // Reading the body.
+ int bodyBytesToRead = Math.min(data.bytesLeft(), totalSectionLength - bytesRead);
+ data.readBytes(sectionData.data, bytesRead, bodyBytesToRead);
+ bytesRead += bodyBytesToRead;
+ if (bytesRead == totalSectionLength) {
+ if (sectionSyntaxIndicator) {
+ // This section has common syntax as defined in ISO/IEC 13818-1, section 2.4.4.11.
+ if (Util.crc32(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) {
+ // The CRC is invalid so discard the section.
+ waitingForPayloadStart = true;
+ return;
+ }
+ sectionData.reset(totalSectionLength - 4); // Exclude the CRC_32 field.
+ } else {
+ // This is a private section with private defined syntax.
+ sectionData.reset(totalSectionLength);
+ }
+ reader.consume(sectionData);
+ bytesRead = 0;
+ }
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java
new file mode 100644
index 0000000000..88ea482be4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java
@@ -0,0 +1,73 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.CeaUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.List;
+
+/** Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. */
+public final class SeiReader {
+
+ private final List<Format> closedCaptionFormats;
+ private final TrackOutput[] outputs;
+
+ /**
+ * @param closedCaptionFormats A list of formats for the closed caption channels to expose.
+ */
+ public SeiReader(List<Format> closedCaptionFormats) {
+ this.closedCaptionFormats = closedCaptionFormats;
+ outputs = new TrackOutput[closedCaptionFormats.size()];
+ }
+
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ for (int i = 0; i < outputs.length; i++) {
+ idGenerator.generateNewId();
+ TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
+ Format channelFormat = closedCaptionFormats.get(i);
+ String channelMimeType = channelFormat.sampleMimeType;
+ Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType)
+ || MimeTypes.APPLICATION_CEA708.equals(channelMimeType),
+ "Invalid closed caption mime type provided: " + channelMimeType);
+ String formatId = channelFormat.id != null ? channelFormat.id : idGenerator.getFormatId();
+ output.format(
+ Format.createTextSampleFormat(
+ formatId,
+ channelMimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ channelFormat.selectionFlags,
+ channelFormat.language,
+ channelFormat.accessibilityChannel,
+ /* drmInitData= */ null,
+ Format.OFFSET_SAMPLE_RELATIVE,
+ channelFormat.initializationData));
+ outputs[i] = output;
+ }
+ }
+
+ public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {
+ CeaUtil.consume(pesTimeUs, seiBuffer, outputs);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java
new file mode 100644
index 0000000000..17223bad7c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java
@@ -0,0 +1,62 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Parses splice info sections as defined by SCTE35.
+ */
+public final class SpliceInfoSectionReader implements SectionPayloadReader {
+
+ private TimestampAdjuster timestampAdjuster;
+ private TrackOutput output;
+ private boolean formatDeclared;
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TsPayloadReader.TrackIdGenerator idGenerator) {
+ this.timestampAdjuster = timestampAdjuster;
+ idGenerator.generateNewId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
+ output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35,
+ null, Format.NO_VALUE, null));
+ }
+
+ @Override
+ public void consume(ParsableByteArray sectionData) {
+ if (!formatDeclared) {
+ if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) {
+ // There is not enough information to initialize the timestamp adjuster.
+ return;
+ }
+ output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35,
+ timestampAdjuster.getTimestampOffsetUs()));
+ formatDeclared = true;
+ }
+ int sampleSize = sectionData.bytesLeft();
+ output.sampleData(sectionData, sampleSize);
+ output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME,
+ sampleSize, 0, null);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java
new file mode 100644
index 0000000000..136691bdaf
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.BinarySearchSeeker;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A seeker that supports seeking within TS stream using binary search.
+ *
+ * <p>This seeker uses the first and last PCR values within the stream, as well as the stream
+ * duration to interpolate the PCR value of the seeking position. Then it performs binary search
+ * within the stream to find a packets whose PCR value is within {@link #SEEK_TOLERANCE_US} from the
+ * target PCR.
+ */
+/* package */ final class TsBinarySearchSeeker extends BinarySearchSeeker {
+
+ private static final long SEEK_TOLERANCE_US = 100_000;
+ private static final int MINIMUM_SEARCH_RANGE_BYTES = 5 * TsExtractor.TS_PACKET_SIZE;
+ private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE;
+
+ public TsBinarySearchSeeker(
+ TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) {
+ super(
+ new DefaultSeekTimestampConverter(),
+ new TsPcrSeeker(pcrPid, pcrTimestampAdjuster),
+ streamDurationUs,
+ /* floorTimePosition= */ 0,
+ /* ceilingTimePosition= */ streamDurationUs + 1,
+ /* floorBytePosition= */ 0,
+ /* ceilingBytePosition= */ inputLength,
+ /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE,
+ MINIMUM_SEARCH_RANGE_BYTES);
+ }
+
+ /**
+ * A {@link TimestampSeeker} implementation that looks for a given PCR timestamp at a given
+ * position in a TS stream.
+ *
+ * <p>Given a PCR timestamp, and a position within a TS stream, this seeker will peek up to {@link
+ * #TIMESTAMP_SEARCH_BYTES} from that stream position, look for all packets with PID equal to
+ * PCR_PID, and then compare the PCR timestamps (if available) of these packets to the target
+ * timestamp.
+ */
+ private static final class TsPcrSeeker implements TimestampSeeker {
+
+ private final TimestampAdjuster pcrTimestampAdjuster;
+ private final ParsableByteArray packetBuffer;
+ private final int pcrPid;
+
+ public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) {
+ this.pcrPid = pcrPid;
+ this.pcrTimestampAdjuster = pcrTimestampAdjuster;
+ packetBuffer = new ParsableByteArray();
+ }
+
+ @Override
+ public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
+ throws IOException, InterruptedException {
+ long inputPosition = input.getPosition();
+ int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition);
+
+ packetBuffer.reset(bytesToSearch);
+ input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
+
+ return searchForPcrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
+ }
+
+ private TimestampSearchResult searchForPcrValueInBuffer(
+ ParsableByteArray packetBuffer, long targetPcrTimeUs, long bufferStartOffset) {
+ int limit = packetBuffer.limit();
+
+ long startOfLastPacketPosition = C.POSITION_UNSET;
+ long endOfLastPacketPosition = C.POSITION_UNSET;
+ long lastPcrTimeUsInRange = C.TIME_UNSET;
+
+ while (packetBuffer.bytesLeft() >= TsExtractor.TS_PACKET_SIZE) {
+ int startOfPacket =
+ TsUtil.findSyncBytePosition(packetBuffer.data, packetBuffer.getPosition(), limit);
+ int endOfPacket = startOfPacket + TsExtractor.TS_PACKET_SIZE;
+ if (endOfPacket > limit) {
+ break;
+ }
+ long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, startOfPacket, pcrPid);
+ if (pcrValue != C.TIME_UNSET) {
+ long pcrTimeUs = pcrTimestampAdjuster.adjustTsTimestamp(pcrValue);
+ if (pcrTimeUs > targetPcrTimeUs) {
+ if (lastPcrTimeUsInRange == C.TIME_UNSET) {
+ // First PCR timestamp is already over target.
+ return TimestampSearchResult.overestimatedResult(pcrTimeUs, bufferStartOffset);
+ } else {
+ // Last PCR timestamp < target timestamp < this timestamp.
+ return TimestampSearchResult.targetFoundResult(
+ bufferStartOffset + startOfLastPacketPosition);
+ }
+ } else if (pcrTimeUs + SEEK_TOLERANCE_US > targetPcrTimeUs) {
+ long startOfPacketInStream = bufferStartOffset + startOfPacket;
+ return TimestampSearchResult.targetFoundResult(startOfPacketInStream);
+ }
+
+ lastPcrTimeUsInRange = pcrTimeUs;
+ startOfLastPacketPosition = startOfPacket;
+ }
+ packetBuffer.setPosition(endOfPacket);
+ endOfLastPacketPosition = endOfPacket;
+ }
+
+ if (lastPcrTimeUsInRange != C.TIME_UNSET) {
+ long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition;
+ return TimestampSearchResult.underestimatedResult(
+ lastPcrTimeUsInRange, endOfLastPacketPositionInStream);
+ } else {
+ return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
+ }
+ }
+
+ @Override
+ public void onSeekFinished() {
+ packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java
new file mode 100644
index 0000000000..ed4b66a7e4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A reader that can extract the approximate duration from a given MPEG transport stream (TS).
+ *
+ * <p>This reader extracts the duration by reading PCR values of the PCR PID packets at the start
+ * and at the end of the stream, calculating the difference, and converting that into stream
+ * duration. This reader also handles the case when a single PCR wraparound takes place within the
+ * stream, which can make PCR values at the beginning of the stream larger than PCR values at the
+ * end. This class can only be used once to read duration from a given stream, and the usage of the
+ * class is not thread-safe, so all calls should be made from the same thread.
+ */
+/* package */ final class TsDurationReader {
+
+ private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE;
+
+ private final TimestampAdjuster pcrTimestampAdjuster;
+ private final ParsableByteArray packetBuffer;
+
+ private boolean isDurationRead;
+ private boolean isFirstPcrValueRead;
+ private boolean isLastPcrValueRead;
+
+ private long firstPcrValue;
+ private long lastPcrValue;
+ private long durationUs;
+
+ /* package */ TsDurationReader() {
+ pcrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);
+ firstPcrValue = C.TIME_UNSET;
+ lastPcrValue = C.TIME_UNSET;
+ durationUs = C.TIME_UNSET;
+ packetBuffer = new ParsableByteArray();
+ }
+
+ /** Returns true if a TS duration has been read. */
+ public boolean isDurationReadFinished() {
+ return isDurationRead;
+ }
+
+ /**
+ * Reads a TS duration from the input, using the given PCR PID.
+ *
+ * <p>This reader reads the duration by reading PCR values of the PCR PID packets at the start and
+ * at the end of the stream, calculating the difference, and converting that into stream duration.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
+ * to hold the position of the required seek.
+ * @param pcrPid The PID of the packet stream within this TS stream that contains PCR values.
+ * @return One of the {@code RESULT_} values defined in {@link Extractor}.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public @Extractor.ReadResult int readDuration(
+ ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
+ throws IOException, InterruptedException {
+ if (pcrPid <= 0) {
+ return finishReadDuration(input);
+ }
+ if (!isLastPcrValueRead) {
+ return readLastPcrValue(input, seekPositionHolder, pcrPid);
+ }
+ if (lastPcrValue == C.TIME_UNSET) {
+ return finishReadDuration(input);
+ }
+ if (!isFirstPcrValueRead) {
+ return readFirstPcrValue(input, seekPositionHolder, pcrPid);
+ }
+ if (firstPcrValue == C.TIME_UNSET) {
+ return finishReadDuration(input);
+ }
+
+ long minPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(firstPcrValue);
+ long maxPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(lastPcrValue);
+ durationUs = maxPcrPositionUs - minPcrPositionUs;
+ return finishReadDuration(input);
+ }
+
+ /**
+ * Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder, int)}.
+ */
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /**
+ * Returns the {@link TimestampAdjuster} that this class uses to adjust timestamps read from the
+ * input TS stream.
+ */
+ public TimestampAdjuster getPcrTimestampAdjuster() {
+ return pcrTimestampAdjuster;
+ }
+
+ private int finishReadDuration(ExtractorInput input) {
+ packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
+ isDurationRead = true;
+ input.resetPeekPosition();
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
+ throws IOException, InterruptedException {
+ int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength());
+ int searchStartPosition = 0;
+ if (input.getPosition() != searchStartPosition) {
+ seekPositionHolder.position = searchStartPosition;
+ return Extractor.RESULT_SEEK;
+ }
+
+ packetBuffer.reset(bytesToSearch);
+ input.resetPeekPosition();
+ input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
+
+ firstPcrValue = readFirstPcrValueFromBuffer(packetBuffer, pcrPid);
+ isFirstPcrValueRead = true;
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private long readFirstPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) {
+ int searchStartPosition = packetBuffer.getPosition();
+ int searchEndPosition = packetBuffer.limit();
+ for (int searchPosition = searchStartPosition;
+ searchPosition < searchEndPosition;
+ searchPosition++) {
+ if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) {
+ continue;
+ }
+ long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid);
+ if (pcrValue != C.TIME_UNSET) {
+ return pcrValue;
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+ private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
+ throws IOException, InterruptedException {
+ long inputLength = input.getLength();
+ int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength);
+ long searchStartPosition = inputLength - bytesToSearch;
+ if (input.getPosition() != searchStartPosition) {
+ seekPositionHolder.position = searchStartPosition;
+ return Extractor.RESULT_SEEK;
+ }
+
+ packetBuffer.reset(bytesToSearch);
+ input.resetPeekPosition();
+ input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
+
+ lastPcrValue = readLastPcrValueFromBuffer(packetBuffer, pcrPid);
+ isLastPcrValueRead = true;
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private long readLastPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) {
+ int searchStartPosition = packetBuffer.getPosition();
+ int searchEndPosition = packetBuffer.limit();
+ for (int searchPosition = searchEndPosition - 1;
+ searchPosition >= searchStartPosition;
+ searchPosition--) {
+ if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) {
+ continue;
+ }
+ long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid);
+ if (pcrValue != C.TIME_UNSET) {
+ return pcrValue;
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
new file mode 100644
index 0000000000..a52e56bd32
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
@@ -0,0 +1,698 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_PAYLOAD_UNIT_START_INDICATOR;
+
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory.Flags;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import org.mozilla.thirdparty.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.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Extracts data from the MPEG-2 TS container format.
+ */
+public final class TsExtractor implements Extractor {
+
+ /** Factory for {@link TsExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new TsExtractor()};
+
+ /**
+ * Modes for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} or {@link
+ * #MODE_HLS}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({MODE_MULTI_PMT, MODE_SINGLE_PMT, MODE_HLS})
+ public @interface Mode {}
+
+ /**
+ * Behave as defined in ISO/IEC 13818-1.
+ */
+ public static final int MODE_MULTI_PMT = 0;
+ /**
+ * Assume only one PMT will be contained in the stream, even if more are declared by the PAT.
+ */
+ public static final int MODE_SINGLE_PMT = 1;
+ /**
+ * Enable single PMT mode, map {@link TrackOutput}s by their type (instead of PID) and ignore
+ * continuity counters.
+ */
+ public static final int MODE_HLS = 2;
+
+ public static final int TS_STREAM_TYPE_MPA = 0x03;
+ public static final int TS_STREAM_TYPE_MPA_LSF = 0x04;
+ public static final int TS_STREAM_TYPE_AAC_ADTS = 0x0F;
+ public static final int TS_STREAM_TYPE_AAC_LATM = 0x11;
+ public static final int TS_STREAM_TYPE_AC3 = 0x81;
+ public static final int TS_STREAM_TYPE_DTS = 0x8A;
+ public static final int TS_STREAM_TYPE_HDMV_DTS = 0x82;
+ public static final int TS_STREAM_TYPE_E_AC3 = 0x87;
+ public static final int TS_STREAM_TYPE_AC4 = 0xAC; // DVB/ATSC AC-4 Descriptor
+ public static final int TS_STREAM_TYPE_H262 = 0x02;
+ public static final int TS_STREAM_TYPE_H264 = 0x1B;
+ public static final int TS_STREAM_TYPE_H265 = 0x24;
+ public static final int TS_STREAM_TYPE_ID3 = 0x15;
+ public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86;
+ public static final int TS_STREAM_TYPE_DVBSUBS = 0x59;
+
+ public static final int TS_PACKET_SIZE = 188;
+ public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
+
+ private static final int TS_PAT_PID = 0;
+ private static final int MAX_PID_PLUS_ONE = 0x2000;
+
+ private static final long AC3_FORMAT_IDENTIFIER = 0x41432d33;
+ private static final long E_AC3_FORMAT_IDENTIFIER = 0x45414333;
+ private static final long AC4_FORMAT_IDENTIFIER = 0x41432d34;
+ private static final long HEVC_FORMAT_IDENTIFIER = 0x48455643;
+
+ private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50;
+ private static final int SNIFF_TS_PACKET_COUNT = 5;
+
+ private final @Mode int mode;
+ private final List<TimestampAdjuster> timestampAdjusters;
+ private final ParsableByteArray tsPacketBuffer;
+ private final SparseIntArray continuityCounters;
+ private final TsPayloadReader.Factory payloadReaderFactory;
+ private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
+ private final SparseBooleanArray trackIds;
+ private final SparseBooleanArray trackPids;
+ private final TsDurationReader durationReader;
+
+ // Accessed only by the loading thread.
+ private TsBinarySearchSeeker tsBinarySearchSeeker;
+ private ExtractorOutput output;
+ private int remainingPmts;
+ private boolean tracksEnded;
+ private boolean hasOutputSeekMap;
+ private boolean pendingSeekToStart;
+ private TsPayloadReader id3Reader;
+ private int bytesSinceLastSync;
+ private int pcrPid;
+
+ public TsExtractor() {
+ this(0);
+ }
+
+ /**
+ * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory}
+ * {@code FLAG_*} values that control the behavior of the payload readers.
+ */
+ public TsExtractor(@Flags int defaultTsPayloadReaderFlags) {
+ this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags);
+ }
+
+ /**
+ * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT}
+ * and {@link #MODE_HLS}.
+ * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory}
+ * {@code FLAG_*} values that control the behavior of the payload readers.
+ */
+ public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) {
+ this(
+ mode,
+ new TimestampAdjuster(0),
+ new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags));
+ }
+
+ /**
+ * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT}
+ * and {@link #MODE_HLS}.
+ * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
+ * @param payloadReaderFactory Factory for injecting a custom set of payload readers.
+ */
+ public TsExtractor(
+ @Mode int mode,
+ TimestampAdjuster timestampAdjuster,
+ TsPayloadReader.Factory payloadReaderFactory) {
+ this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory);
+ this.mode = mode;
+ if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) {
+ timestampAdjusters = Collections.singletonList(timestampAdjuster);
+ } else {
+ timestampAdjusters = new ArrayList<>();
+ timestampAdjusters.add(timestampAdjuster);
+ }
+ tsPacketBuffer = new ParsableByteArray(new byte[BUFFER_SIZE], 0);
+ trackIds = new SparseBooleanArray();
+ trackPids = new SparseBooleanArray();
+ tsPayloadReaders = new SparseArray<>();
+ continuityCounters = new SparseIntArray();
+ durationReader = new TsDurationReader();
+ pcrPid = -1;
+ resetPayloadReaders();
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ byte[] buffer = tsPacketBuffer.data;
+ input.peekFully(buffer, 0, TS_PACKET_SIZE * SNIFF_TS_PACKET_COUNT);
+ for (int startPosCandidate = 0; startPosCandidate < TS_PACKET_SIZE; startPosCandidate++) {
+ // Try to identify at least SNIFF_TS_PACKET_COUNT packets starting with TS_SYNC_BYTE.
+ boolean isSyncBytePatternCorrect = true;
+ for (int i = 0; i < SNIFF_TS_PACKET_COUNT; i++) {
+ if (buffer[startPosCandidate + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) {
+ isSyncBytePatternCorrect = false;
+ break;
+ }
+ }
+ if (isSyncBytePatternCorrect) {
+ input.skipFully(startPosCandidate);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.output = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ Assertions.checkState(mode != MODE_HLS);
+ int timestampAdjustersCount = timestampAdjusters.size();
+ for (int i = 0; i < timestampAdjustersCount; i++) {
+ TimestampAdjuster timestampAdjuster = timestampAdjusters.get(i);
+ boolean hasNotEncounteredFirstTimestamp =
+ timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET;
+ if (hasNotEncounteredFirstTimestamp
+ || (timestampAdjuster.getTimestampOffsetUs() != 0
+ && timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) {
+ // - If a track in the TS stream has not encountered any sample, it's going to treat the
+ // first sample encountered as timestamp 0, which is incorrect. So we have to set the first
+ // sample timestamp for that track manually.
+ // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a
+ // different position, we need to set the first sample timestamp manually again.
+ timestampAdjuster.reset();
+ timestampAdjuster.setFirstSampleTimestampUs(timeUs);
+ }
+ }
+ if (timeUs != 0 && tsBinarySearchSeeker != null) {
+ tsBinarySearchSeeker.setSeekTargetUs(timeUs);
+ }
+ tsPacketBuffer.reset();
+ continuityCounters.clear();
+ for (int i = 0; i < tsPayloadReaders.size(); i++) {
+ tsPayloadReaders.valueAt(i).seek();
+ }
+ bytesSinceLastSync = 0;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ long inputLength = input.getLength();
+ if (tracksEnded) {
+ boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS;
+ if (canReadDuration && !durationReader.isDurationReadFinished()) {
+ return durationReader.readDuration(input, seekPosition, pcrPid);
+ }
+ maybeOutputSeekMap(inputLength);
+
+ if (pendingSeekToStart) {
+ pendingSeekToStart = false;
+ seek(/* position= */ 0, /* timeUs= */ 0);
+ if (input.getPosition() != 0) {
+ seekPosition.position = 0;
+ return RESULT_SEEK;
+ }
+ }
+
+ if (tsBinarySearchSeeker != null && tsBinarySearchSeeker.isSeeking()) {
+ return tsBinarySearchSeeker.handlePendingSeek(input, seekPosition);
+ }
+ }
+
+ if (!fillBufferWithAtLeastOnePacket(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ int endOfPacket = findEndOfFirstTsPacketInBuffer();
+ int limit = tsPacketBuffer.limit();
+ if (endOfPacket > limit) {
+ return RESULT_CONTINUE;
+ }
+
+ @TsPayloadReader.Flags int packetHeaderFlags = 0;
+
+ // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
+ int tsPacketHeader = tsPacketBuffer.readInt();
+ if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator
+ // There are uncorrectable errors in this packet.
+ tsPacketBuffer.setPosition(endOfPacket);
+ return RESULT_CONTINUE;
+ }
+ packetHeaderFlags |= (tsPacketHeader & 0x400000) != 0 ? FLAG_PAYLOAD_UNIT_START_INDICATOR : 0;
+ // Ignoring transport_priority (tsPacketHeader & 0x200000)
+ int pid = (tsPacketHeader & 0x1FFF00) >> 8;
+ // Ignoring transport_scrambling_control (tsPacketHeader & 0xC0)
+ boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0;
+ boolean payloadExists = (tsPacketHeader & 0x10) != 0;
+
+ TsPayloadReader payloadReader = payloadExists ? tsPayloadReaders.get(pid) : null;
+ if (payloadReader == null) {
+ tsPacketBuffer.setPosition(endOfPacket);
+ return RESULT_CONTINUE;
+ }
+
+ // Discontinuity check.
+ if (mode != MODE_HLS) {
+ int continuityCounter = tsPacketHeader & 0xF;
+ int previousCounter = continuityCounters.get(pid, continuityCounter - 1);
+ continuityCounters.put(pid, continuityCounter);
+ if (previousCounter == continuityCounter) {
+ // Duplicate packet found.
+ tsPacketBuffer.setPosition(endOfPacket);
+ return RESULT_CONTINUE;
+ } else if (continuityCounter != ((previousCounter + 1) & 0xF)) {
+ // Discontinuity found.
+ payloadReader.seek();
+ }
+ }
+
+ // Skip the adaptation field.
+ if (adaptationFieldExists) {
+ int adaptationFieldLength = tsPacketBuffer.readUnsignedByte();
+ int adaptationFieldFlags = tsPacketBuffer.readUnsignedByte();
+
+ packetHeaderFlags |=
+ (adaptationFieldFlags & 0x40) != 0 // random_access_indicator.
+ ? TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR
+ : 0;
+ tsPacketBuffer.skipBytes(adaptationFieldLength - 1 /* flags */);
+ }
+
+ // Read the payload.
+ boolean wereTracksEnded = tracksEnded;
+ if (shouldConsumePacketPayload(pid)) {
+ tsPacketBuffer.setLimit(endOfPacket);
+ payloadReader.consume(tsPacketBuffer, packetHeaderFlags);
+ tsPacketBuffer.setLimit(limit);
+ }
+ if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) {
+ // We have read all tracks from all PMTs in this non-live stream. Now seek to the beginning
+ // and read again to make sure we output all media, including any contained in packets prior
+ // to those containing the track information.
+ pendingSeekToStart = true;
+ }
+
+ tsPacketBuffer.setPosition(endOfPacket);
+ return RESULT_CONTINUE;
+ }
+
+ // Internals.
+
+ private void maybeOutputSeekMap(long inputLength) {
+ if (!hasOutputSeekMap) {
+ hasOutputSeekMap = true;
+ if (durationReader.getDurationUs() != C.TIME_UNSET) {
+ tsBinarySearchSeeker =
+ new TsBinarySearchSeeker(
+ durationReader.getPcrTimestampAdjuster(),
+ durationReader.getDurationUs(),
+ inputLength,
+ pcrPid);
+ output.seekMap(tsBinarySearchSeeker.getSeekMap());
+ } else {
+ output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));
+ }
+ }
+ }
+
+ private boolean fillBufferWithAtLeastOnePacket(ExtractorInput input)
+ throws IOException, InterruptedException {
+ byte[] data = tsPacketBuffer.data;
+ // Shift bytes to the start of the buffer if there isn't enough space left at the end.
+ if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) {
+ int bytesLeft = tsPacketBuffer.bytesLeft();
+ if (bytesLeft > 0) {
+ System.arraycopy(data, tsPacketBuffer.getPosition(), data, 0, bytesLeft);
+ }
+ tsPacketBuffer.reset(data, bytesLeft);
+ }
+ // Read more bytes until we have at least one packet.
+ while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) {
+ int limit = tsPacketBuffer.limit();
+ int read = input.read(data, limit, BUFFER_SIZE - limit);
+ if (read == C.RESULT_END_OF_INPUT) {
+ return false;
+ }
+ tsPacketBuffer.setLimit(limit + read);
+ }
+ return true;
+ }
+
+ /**
+ * Returns the position of the end of the first TS packet (exclusive) in the packet buffer.
+ *
+ * <p>This may be a position beyond the buffer limit if the packet has not been read fully into
+ * the buffer, or if no packet could be found within the buffer.
+ */
+ private int findEndOfFirstTsPacketInBuffer() throws ParserException {
+ int searchStart = tsPacketBuffer.getPosition();
+ int limit = tsPacketBuffer.limit();
+ int syncBytePosition = TsUtil.findSyncBytePosition(tsPacketBuffer.data, searchStart, limit);
+ // Discard all bytes before the sync byte.
+ // If sync byte is not found, this means discard the whole buffer.
+ tsPacketBuffer.setPosition(syncBytePosition);
+ int endOfPacket = syncBytePosition + TS_PACKET_SIZE;
+ if (endOfPacket > limit) {
+ bytesSinceLastSync += syncBytePosition - searchStart;
+ if (mode == MODE_HLS && bytesSinceLastSync > TS_PACKET_SIZE * 2) {
+ throw new ParserException("Cannot find sync byte. Most likely not a Transport Stream.");
+ }
+ } else {
+ // We have found a packet within the buffer.
+ bytesSinceLastSync = 0;
+ }
+ return endOfPacket;
+ }
+
+ private boolean shouldConsumePacketPayload(int packetPid) {
+ return mode == MODE_HLS
+ || tracksEnded
+ || !trackPids.get(packetPid, /* valueIfKeyNotFound= */ false); // It's a PSI packet
+ }
+
+ private void resetPayloadReaders() {
+ trackIds.clear();
+ tsPayloadReaders.clear();
+ SparseArray<TsPayloadReader> initialPayloadReaders =
+ payloadReaderFactory.createInitialPayloadReaders();
+ int initialPayloadReadersSize = initialPayloadReaders.size();
+ for (int i = 0; i < initialPayloadReadersSize; i++) {
+ tsPayloadReaders.put(initialPayloadReaders.keyAt(i), initialPayloadReaders.valueAt(i));
+ }
+ tsPayloadReaders.put(TS_PAT_PID, new SectionReader(new PatReader()));
+ id3Reader = null;
+ }
+
+ /**
+ * Parses Program Association Table data.
+ */
+ private class PatReader implements SectionPayloadReader {
+
+ private final ParsableBitArray patScratch;
+
+ public PatReader() {
+ patScratch = new ParsableBitArray(new byte[4]);
+ }
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator) {
+ // Do nothing.
+ }
+
+ @Override
+ public void consume(ParsableByteArray sectionData) {
+ int tableId = sectionData.readUnsignedByte();
+ if (tableId != 0x00 /* program_association_section */) {
+ // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment.
+ return;
+ }
+ // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12),
+ // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1),
+ // section_number (8), last_section_number (8)
+ sectionData.skipBytes(7);
+
+ int programCount = sectionData.bytesLeft() / 4;
+ for (int i = 0; i < programCount; i++) {
+ sectionData.readBytes(patScratch, 4);
+ int programNumber = patScratch.readBits(16);
+ patScratch.skipBits(3); // reserved (3)
+ if (programNumber == 0) {
+ patScratch.skipBits(13); // network_PID (13)
+ } else {
+ int pid = patScratch.readBits(13);
+ tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid)));
+ remainingPmts++;
+ }
+ }
+ if (mode != MODE_HLS) {
+ tsPayloadReaders.remove(TS_PAT_PID);
+ }
+ }
+
+ }
+
+ /**
+ * Parses Program Map Table.
+ */
+ private class PmtReader implements SectionPayloadReader {
+
+ private static final int TS_PMT_DESC_REGISTRATION = 0x05;
+ private static final int TS_PMT_DESC_ISO639_LANG = 0x0A;
+ private static final int TS_PMT_DESC_AC3 = 0x6A;
+ private static final int TS_PMT_DESC_EAC3 = 0x7A;
+ private static final int TS_PMT_DESC_DTS = 0x7B;
+ private static final int TS_PMT_DESC_DVB_EXT = 0x7F;
+ private static final int TS_PMT_DESC_DVBSUBS = 0x59;
+
+ private static final int TS_PMT_DESC_DVB_EXT_AC4 = 0x15;
+
+ private final ParsableBitArray pmtScratch;
+ private final SparseArray<TsPayloadReader> trackIdToReaderScratch;
+ private final SparseIntArray trackIdToPidScratch;
+ private final int pid;
+
+ public PmtReader(int pid) {
+ pmtScratch = new ParsableBitArray(new byte[5]);
+ trackIdToReaderScratch = new SparseArray<>();
+ trackIdToPidScratch = new SparseIntArray();
+ this.pid = pid;
+ }
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator) {
+ // Do nothing.
+ }
+
+ @Override
+ public void consume(ParsableByteArray sectionData) {
+ int tableId = sectionData.readUnsignedByte();
+ if (tableId != 0x02 /* TS_program_map_section */) {
+ // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment.
+ return;
+ }
+ // TimestampAdjuster assignment.
+ TimestampAdjuster timestampAdjuster;
+ if (mode == MODE_SINGLE_PMT || mode == MODE_HLS || remainingPmts == 1) {
+ timestampAdjuster = timestampAdjusters.get(0);
+ } else {
+ timestampAdjuster = new TimestampAdjuster(
+ timestampAdjusters.get(0).getFirstSampleTimestampUs());
+ timestampAdjusters.add(timestampAdjuster);
+ }
+
+ // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12)
+ sectionData.skipBytes(2);
+ int programNumber = sectionData.readUnsignedShort();
+
+ // Skip 3 bytes (24 bits), including:
+ // reserved (2), version_number (5), current_next_indicator (1), section_number (8),
+ // last_section_number (8)
+ sectionData.skipBytes(3);
+
+ sectionData.readBytes(pmtScratch, 2);
+ // reserved (3), PCR_PID (13)
+ pmtScratch.skipBits(3);
+ pcrPid = pmtScratch.readBits(13);
+
+ // Read program_info_length.
+ sectionData.readBytes(pmtScratch, 2);
+ pmtScratch.skipBits(4);
+ int programInfoLength = pmtScratch.readBits(12);
+
+ // Skip the descriptors.
+ sectionData.skipBytes(programInfoLength);
+
+ if (mode == MODE_HLS && id3Reader == null) {
+ // Setup an ID3 track regardless of whether there's a corresponding entry, in case one
+ // appears intermittently during playback. See [Internal: b/20261500].
+ EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, null, Util.EMPTY_BYTE_ARRAY);
+ id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo);
+ id3Reader.init(timestampAdjuster, output,
+ new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE));
+ }
+
+ trackIdToReaderScratch.clear();
+ trackIdToPidScratch.clear();
+ int remainingEntriesLength = sectionData.bytesLeft();
+ while (remainingEntriesLength > 0) {
+ sectionData.readBytes(pmtScratch, 5);
+ int streamType = pmtScratch.readBits(8);
+ pmtScratch.skipBits(3); // reserved
+ int elementaryPid = pmtScratch.readBits(13);
+ pmtScratch.skipBits(4); // reserved
+ int esInfoLength = pmtScratch.readBits(12); // ES_info_length.
+ EsInfo esInfo = readEsInfo(sectionData, esInfoLength);
+ if (streamType == 0x06) {
+ streamType = esInfo.streamType;
+ }
+ remainingEntriesLength -= esInfoLength + 5;
+
+ int trackId = mode == MODE_HLS ? streamType : elementaryPid;
+ if (trackIds.get(trackId)) {
+ continue;
+ }
+
+ TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader
+ : payloadReaderFactory.createPayloadReader(streamType, esInfo);
+ if (mode != MODE_HLS
+ || elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) {
+ trackIdToPidScratch.put(trackId, elementaryPid);
+ trackIdToReaderScratch.put(trackId, reader);
+ }
+ }
+
+ int trackIdCount = trackIdToPidScratch.size();
+ for (int i = 0; i < trackIdCount; i++) {
+ int trackId = trackIdToPidScratch.keyAt(i);
+ int trackPid = trackIdToPidScratch.valueAt(i);
+ trackIds.put(trackId, true);
+ trackPids.put(trackPid, true);
+ TsPayloadReader reader = trackIdToReaderScratch.valueAt(i);
+ if (reader != null) {
+ if (reader != id3Reader) {
+ reader.init(timestampAdjuster, output,
+ new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE));
+ }
+ tsPayloadReaders.put(trackPid, reader);
+ }
+ }
+
+ if (mode == MODE_HLS) {
+ if (!tracksEnded) {
+ output.endTracks();
+ remainingPmts = 0;
+ tracksEnded = true;
+ }
+ } else {
+ tsPayloadReaders.remove(pid);
+ remainingPmts = mode == MODE_SINGLE_PMT ? 0 : remainingPmts - 1;
+ if (remainingPmts == 0) {
+ output.endTracks();
+ tracksEnded = true;
+ }
+ }
+ }
+
+ /**
+ * Returns the stream info read from the available descriptors. Sets {@code data}'s position to
+ * the end of the descriptors.
+ *
+ * @param data A buffer with its position set to the start of the first descriptor.
+ * @param length The length of descriptors to read from the current position in {@code data}.
+ * @return The stream info read from the available descriptors.
+ */
+ private EsInfo readEsInfo(ParsableByteArray data, int length) {
+ int descriptorsStartPosition = data.getPosition();
+ int descriptorsEndPosition = descriptorsStartPosition + length;
+ int streamType = -1;
+ String language = null;
+ List<DvbSubtitleInfo> dvbSubtitleInfos = null;
+ while (data.getPosition() < descriptorsEndPosition) {
+ int descriptorTag = data.readUnsignedByte();
+ int descriptorLength = data.readUnsignedByte();
+ int positionOfNextDescriptor = data.getPosition() + descriptorLength;
+ if (descriptorTag == TS_PMT_DESC_REGISTRATION) { // registration_descriptor
+ long formatIdentifier = data.readUnsignedInt();
+ if (formatIdentifier == AC3_FORMAT_IDENTIFIER) {
+ streamType = TS_STREAM_TYPE_AC3;
+ } else if (formatIdentifier == E_AC3_FORMAT_IDENTIFIER) {
+ streamType = TS_STREAM_TYPE_E_AC3;
+ } else if (formatIdentifier == AC4_FORMAT_IDENTIFIER) {
+ streamType = TS_STREAM_TYPE_AC4;
+ } else if (formatIdentifier == HEVC_FORMAT_IDENTIFIER) {
+ streamType = TS_STREAM_TYPE_H265;
+ }
+ } else if (descriptorTag == TS_PMT_DESC_AC3) { // AC-3_descriptor in DVB (ETSI EN 300 468)
+ streamType = TS_STREAM_TYPE_AC3;
+ } else if (descriptorTag == TS_PMT_DESC_EAC3) { // enhanced_AC-3_descriptor
+ streamType = TS_STREAM_TYPE_E_AC3;
+ } else if (descriptorTag == TS_PMT_DESC_DVB_EXT) {
+ // Extension descriptor in DVB (ETSI EN 300 468).
+ int descriptorTagExt = data.readUnsignedByte();
+ if (descriptorTagExt == TS_PMT_DESC_DVB_EXT_AC4) {
+ // AC-4_descriptor in DVB (ETSI EN 300 468).
+ streamType = TS_STREAM_TYPE_AC4;
+ }
+ } else if (descriptorTag == TS_PMT_DESC_DTS) { // DTS_descriptor
+ streamType = TS_STREAM_TYPE_DTS;
+ } else if (descriptorTag == TS_PMT_DESC_ISO639_LANG) {
+ language = data.readString(3).trim();
+ // Audio type is ignored.
+ } else if (descriptorTag == TS_PMT_DESC_DVBSUBS) {
+ streamType = TS_STREAM_TYPE_DVBSUBS;
+ dvbSubtitleInfos = new ArrayList<>();
+ while (data.getPosition() < positionOfNextDescriptor) {
+ String dvbLanguage = data.readString(3).trim();
+ int dvbSubtitlingType = data.readUnsignedByte();
+ byte[] initializationData = new byte[4];
+ data.readBytes(initializationData, 0, 4);
+ dvbSubtitleInfos.add(new DvbSubtitleInfo(dvbLanguage, dvbSubtitlingType,
+ initializationData));
+ }
+ }
+ // Skip unused bytes of current descriptor.
+ data.skipBytes(positionOfNextDescriptor - data.getPosition());
+ }
+ data.setPosition(descriptorsEndPosition);
+ return new EsInfo(streamType, language, dvbSubtitleInfos,
+ Arrays.copyOfRange(data.data, descriptorsStartPosition, descriptorsEndPosition));
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
new file mode 100644
index 0000000000..940c1c7937
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
@@ -0,0 +1,232 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import android.util.SparseArray;
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parses TS packet payload data.
+ */
+public interface TsPayloadReader {
+
+ /**
+ * Factory of {@link TsPayloadReader} instances.
+ */
+ interface Factory {
+
+ /**
+ * Returns the initial mapping from PIDs to payload readers.
+ * <p>
+ * This method allows the injection of payload readers for reserved PIDs, excluding PID 0.
+ *
+ * @return A {@link SparseArray} that maps PIDs to payload readers.
+ */
+ SparseArray<TsPayloadReader> createInitialPayloadReaders();
+
+ /**
+ * Returns a {@link TsPayloadReader} for a given stream type and elementary stream information.
+ * May return null if the stream type is not supported.
+ *
+ * @param streamType Stream type value as defined in the PMT entry or associated descriptors.
+ * @param esInfo Information associated to the elementary stream provided in the PMT.
+ * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid.
+ * {@code null} if the stream is not supported.
+ */
+ TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo);
+
+ }
+
+ /**
+ * Holds information associated with a PMT entry.
+ */
+ final class EsInfo {
+
+ public final int streamType;
+ public final String language;
+ public final List<DvbSubtitleInfo> dvbSubtitleInfos;
+ public final byte[] descriptorBytes;
+
+ /**
+ * @param streamType The type of the stream as defined by the
+ * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}.
+ * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18.
+ * @param dvbSubtitleInfos Information about DVB subtitles associated to the stream.
+ * @param descriptorBytes The descriptor bytes associated to the stream.
+ */
+ public EsInfo(int streamType, String language, List<DvbSubtitleInfo> dvbSubtitleInfos,
+ byte[] descriptorBytes) {
+ this.streamType = streamType;
+ this.language = language;
+ this.dvbSubtitleInfos =
+ dvbSubtitleInfos == null
+ ? Collections.emptyList()
+ : Collections.unmodifiableList(dvbSubtitleInfos);
+ this.descriptorBytes = descriptorBytes;
+ }
+
+ }
+
+ /**
+ * Holds information about a DVB subtitle, as defined in ETSI EN 300 468 V1.11.1 section 6.2.41.
+ */
+ final class DvbSubtitleInfo {
+
+ public final String language;
+ public final int type;
+ public final byte[] initializationData;
+
+ /**
+ * @param language The ISO 639-2 three-letter language code.
+ * @param type The subtitling type.
+ * @param initializationData The composition and ancillary page ids.
+ */
+ public DvbSubtitleInfo(String language, int type, byte[] initializationData) {
+ this.language = language;
+ this.type = type;
+ this.initializationData = initializationData;
+ }
+
+ }
+
+ /**
+ * Generates track ids for initializing {@link TsPayloadReader}s' {@link TrackOutput}s.
+ */
+ final class TrackIdGenerator {
+
+ private static final int ID_UNSET = Integer.MIN_VALUE;
+
+ private final String formatIdPrefix;
+ private final int firstTrackId;
+ private final int trackIdIncrement;
+ private int trackId;
+ private String formatId;
+
+ public TrackIdGenerator(int firstTrackId, int trackIdIncrement) {
+ this(ID_UNSET, firstTrackId, trackIdIncrement);
+ }
+
+ public TrackIdGenerator(int programNumber, int firstTrackId, int trackIdIncrement) {
+ this.formatIdPrefix = programNumber != ID_UNSET ? programNumber + "/" : "";
+ this.firstTrackId = firstTrackId;
+ this.trackIdIncrement = trackIdIncrement;
+ trackId = ID_UNSET;
+ }
+
+ /**
+ * Generates a new set of track and track format ids. Must be called before {@code get*}
+ * methods.
+ */
+ public void generateNewId() {
+ trackId = trackId == ID_UNSET ? firstTrackId : trackId + trackIdIncrement;
+ formatId = formatIdPrefix + trackId;
+ }
+
+ /**
+ * Returns the last generated track id. Must be called after the first {@link #generateNewId()}
+ * call.
+ *
+ * @return The last generated track id.
+ */
+ public int getTrackId() {
+ maybeThrowUninitializedError();
+ return trackId;
+ }
+
+ /**
+ * Returns the last generated format id, with the format {@code "programNumber/trackId"}. If no
+ * {@code programNumber} was provided, the {@code trackId} alone is used as format id. Must be
+ * called after the first {@link #generateNewId()} call.
+ *
+ * @return The last generated format id, with the format {@code "programNumber/trackId"}. If no
+ * {@code programNumber} was provided, the {@code trackId} alone is used as
+ * format id.
+ */
+ public String getFormatId() {
+ maybeThrowUninitializedError();
+ return formatId;
+ }
+
+ private void maybeThrowUninitializedError() {
+ if (trackId == ID_UNSET) {
+ throw new IllegalStateException("generateNewId() must be called before retrieving ids.");
+ }
+ }
+
+ }
+
+ /**
+ * Contextual flags indicating the presence of indicators in the TS packet or PES packet headers.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ FLAG_PAYLOAD_UNIT_START_INDICATOR,
+ FLAG_RANDOM_ACCESS_INDICATOR,
+ FLAG_DATA_ALIGNMENT_INDICATOR
+ })
+ @interface Flags {}
+
+ /** Indicates the presence of the payload_unit_start_indicator in the TS packet header. */
+ int FLAG_PAYLOAD_UNIT_START_INDICATOR = 1;
+ /**
+ * Indicates the presence of the random_access_indicator in the TS packet header adaptation field.
+ */
+ int FLAG_RANDOM_ACCESS_INDICATOR = 1 << 1;
+ /** Indicates the presence of the data_alignment_indicator in the PES header. */
+ int FLAG_DATA_ALIGNMENT_INDICATOR = 1 << 2;
+
+ /**
+ * Initializes the payload reader.
+ *
+ * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
+ * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.
+ * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the
+ * {@link TrackOutput}s.
+ */
+ void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator);
+
+ /**
+ * Notifies the reader that a seek has occurred.
+ *
+ * <p>Following a call to this method, the data passed to the next invocation of {@link #consume}
+ * will not be a continuation of the data that was previously passed. Hence the reader should
+ * reset any internal state.
+ */
+ void seek();
+
+ /**
+ * Consumes the payload of a TS packet.
+ *
+ * @param data The TS packet. The position will be set to the start of the payload.
+ * @param flags See {@link Flags}.
+ * @throws ParserException If the payload could not be parsed.
+ */
+ void consume(ParsableByteArray data, @Flags int flags) throws ParserException;
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java
new file mode 100644
index 0000000000..8cd24ff1e9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+
+/** Utilities method for extracting MPEG-TS streams. */
+public final class TsUtil {
+ /**
+ * Returns the position of the first TS_SYNC_BYTE within the range [startPosition, limitPosition)
+ * from the provided data array, or returns limitPosition if sync byte could not be found.
+ */
+ public static int findSyncBytePosition(byte[] data, int startPosition, int limitPosition) {
+ int position = startPosition;
+ while (position < limitPosition && data[position] != TsExtractor.TS_SYNC_BYTE) {
+ position++;
+ }
+ return position;
+ }
+
+ /**
+ * Returns the PCR value read from a given TS packet.
+ *
+ * @param packetBuffer The buffer that holds the packet.
+ * @param startOfPacket The starting position of the packet in the buffer.
+ * @param pcrPid The PID for valid packets that contain PCR values.
+ * @return The PCR value read from the packet, if its PID is equal to {@code pcrPid} and it
+ * contains a valid PCR value. Returns {@link C#TIME_UNSET} otherwise.
+ */
+ public static long readPcrFromPacket(
+ ParsableByteArray packetBuffer, int startOfPacket, int pcrPid) {
+ packetBuffer.setPosition(startOfPacket);
+ if (packetBuffer.bytesLeft() < 5) {
+ // Header = 4 bytes, adaptationFieldLength = 1 byte.
+ return C.TIME_UNSET;
+ }
+ // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
+ int tsPacketHeader = packetBuffer.readInt();
+ if ((tsPacketHeader & 0x800000) != 0) {
+ // transport_error_indicator != 0 means there are uncorrectable errors in this packet.
+ return C.TIME_UNSET;
+ }
+ int pid = (tsPacketHeader & 0x1FFF00) >> 8;
+ if (pid != pcrPid) {
+ return C.TIME_UNSET;
+ }
+ boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0;
+ if (!adaptationFieldExists) {
+ return C.TIME_UNSET;
+ }
+
+ int adaptationFieldLength = packetBuffer.readUnsignedByte();
+ if (adaptationFieldLength >= 7 && packetBuffer.bytesLeft() >= 7) {
+ int flags = packetBuffer.readUnsignedByte();
+ boolean pcrFlagSet = (flags & 0x10) == 0x10;
+ if (pcrFlagSet) {
+ byte[] pcrBytes = new byte[6];
+ packetBuffer.readBytes(pcrBytes, /* offset= */ 0, pcrBytes.length);
+ return readPcrValueFromPcrBytes(pcrBytes);
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+ /**
+ * Returns the value of PCR base - first 33 bits in big endian order from the PCR bytes.
+ *
+ * <p>We ignore PCR Ext, because it's too small to have any significance.
+ */
+ private static long readPcrValueFromPcrBytes(byte[] pcrBytes) {
+ return (pcrBytes[0] & 0xFFL) << 25
+ | (pcrBytes[1] & 0xFFL) << 17
+ | (pcrBytes[2] & 0xFFL) << 9
+ | (pcrBytes[3] & 0xFFL) << 1
+ | (pcrBytes[4] & 0xFFL) >> 7;
+ }
+
+ private TsUtil() {
+ // Prevent instantiation.
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java
new file mode 100644
index 0000000000..fb56fe379c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.CeaUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.List;
+
+/** Consumes user data, outputting contained CEA-608/708 messages to a {@link TrackOutput}. */
+/* package */ final class UserDataReader {
+
+ private static final int USER_DATA_START_CODE = 0x0001B2;
+
+ private final List<Format> closedCaptionFormats;
+ private final TrackOutput[] outputs;
+
+ public UserDataReader(List<Format> closedCaptionFormats) {
+ this.closedCaptionFormats = closedCaptionFormats;
+ outputs = new TrackOutput[closedCaptionFormats.size()];
+ }
+
+ public void createTracks(
+ ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) {
+ for (int i = 0; i < outputs.length; i++) {
+ idGenerator.generateNewId();
+ TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
+ Format channelFormat = closedCaptionFormats.get(i);
+ String channelMimeType = channelFormat.sampleMimeType;
+ Assertions.checkArgument(
+ MimeTypes.APPLICATION_CEA608.equals(channelMimeType)
+ || MimeTypes.APPLICATION_CEA708.equals(channelMimeType),
+ "Invalid closed caption mime type provided: " + channelMimeType);
+ output.format(
+ Format.createTextSampleFormat(
+ idGenerator.getFormatId(),
+ channelMimeType,
+ /* codecs= */ null,
+ /* bitrate= */ Format.NO_VALUE,
+ channelFormat.selectionFlags,
+ channelFormat.language,
+ channelFormat.accessibilityChannel,
+ /* drmInitData= */ null,
+ Format.OFFSET_SAMPLE_RELATIVE,
+ channelFormat.initializationData));
+ outputs[i] = output;
+ }
+ }
+
+ public void consume(long pesTimeUs, ParsableByteArray userDataPayload) {
+ if (userDataPayload.bytesLeft() < 9) {
+ return;
+ }
+ int userDataStartCode = userDataPayload.readInt();
+ int userDataIdentifier = userDataPayload.readInt();
+ int userDataTypeCode = userDataPayload.readUnsignedByte();
+ if (userDataStartCode == USER_DATA_START_CODE
+ && userDataIdentifier == CeaUtil.USER_DATA_IDENTIFIER_GA94
+ && userDataTypeCode == CeaUtil.USER_DATA_TYPE_CODE_MPEG_CC) {
+ CeaUtil.consumeCcData(pesTimeUs, userDataPayload, outputs);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java
new file mode 100644
index 0000000000..d4ac3ef8e1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java
@@ -0,0 +1,562 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav;
+
+import android.util.Pair;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.WavUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * Extracts data from WAV byte streams.
+ */
+public final class WavExtractor implements Extractor {
+
+ /**
+ * When outputting PCM data to a {@link TrackOutput}, we can choose how many frames are grouped
+ * into each sample, and hence each sample's duration. This is the target number of samples to
+ * output for each second of media, meaning that each sample will have a duration of ~100ms.
+ */
+ private static final int TARGET_SAMPLES_PER_SECOND = 10;
+
+ /** Factory for {@link WavExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()};
+
+ @MonotonicNonNull private ExtractorOutput extractorOutput;
+ @MonotonicNonNull private TrackOutput trackOutput;
+ @MonotonicNonNull private OutputWriter outputWriter;
+ private int dataStartPosition;
+ private long dataEndPosition;
+
+ public WavExtractor() {
+ dataStartPosition = C.POSITION_UNSET;
+ dataEndPosition = C.POSITION_UNSET;
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return WavHeaderReader.peek(input) != null;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
+ output.endTracks();
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ if (outputWriter != null) {
+ outputWriter.reset(timeUs);
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ assertInitialized();
+ if (outputWriter == null) {
+ WavHeader header = WavHeaderReader.peek(input);
+ if (header == null) {
+ // Should only happen if the media wasn't sniffed.
+ throw new ParserException("Unsupported or unrecognized wav header.");
+ }
+
+ if (header.formatType == WavUtil.TYPE_IMA_ADPCM) {
+ outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header);
+ } else if (header.formatType == WavUtil.TYPE_ALAW) {
+ outputWriter =
+ new PassthroughOutputWriter(
+ extractorOutput,
+ trackOutput,
+ header,
+ MimeTypes.AUDIO_ALAW,
+ /* pcmEncoding= */ Format.NO_VALUE);
+ } else if (header.formatType == WavUtil.TYPE_MLAW) {
+ outputWriter =
+ new PassthroughOutputWriter(
+ extractorOutput,
+ trackOutput,
+ header,
+ MimeTypes.AUDIO_MLAW,
+ /* pcmEncoding= */ Format.NO_VALUE);
+ } else {
+ @C.PcmEncoding
+ int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample);
+ if (pcmEncoding == C.ENCODING_INVALID) {
+ throw new ParserException("Unsupported WAV format type: " + header.formatType);
+ }
+ outputWriter =
+ new PassthroughOutputWriter(
+ extractorOutput, trackOutput, header, MimeTypes.AUDIO_RAW, pcmEncoding);
+ }
+ }
+
+ if (dataStartPosition == C.POSITION_UNSET) {
+ Pair<Long, Long> dataBounds = WavHeaderReader.skipToData(input);
+ dataStartPosition = dataBounds.first.intValue();
+ dataEndPosition = dataBounds.second;
+ outputWriter.init(dataStartPosition, dataEndPosition);
+ } else if (input.getPosition() == 0) {
+ input.skipFully(dataStartPosition);
+ }
+
+ Assertions.checkState(dataEndPosition != C.POSITION_UNSET);
+ long bytesLeft = dataEndPosition - input.getPosition();
+ return outputWriter.sampleData(input, bytesLeft) ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
+ }
+
+ @EnsuresNonNull({"extractorOutput", "trackOutput"})
+ private void assertInitialized() {
+ Assertions.checkStateNotNull(trackOutput);
+ Util.castNonNull(extractorOutput);
+ }
+
+ /** Writes to the extractor's output. */
+ private interface OutputWriter {
+
+ /**
+ * Resets the writer.
+ *
+ * @param timeUs The new start position in microseconds.
+ */
+ void reset(long timeUs);
+
+ /**
+ * Initializes the writer.
+ *
+ * <p>Must be called once, before any calls to {@link #sampleData(ExtractorInput, long)}.
+ *
+ * @param dataStartPosition The byte position (inclusive) in the stream at which data starts.
+ * @param dataEndPosition The end position (exclusive) in the stream at which data ends.
+ * @throws ParserException If an error occurs initializing the writer.
+ */
+ void init(int dataStartPosition, long dataEndPosition) throws ParserException;
+
+ /**
+ * Consumes sample data from {@code input}, writing corresponding samples to the extractor's
+ * output.
+ *
+ * <p>Must not be called until after {@link #init(int, long)} has been called.
+ *
+ * @param input The input from which to read.
+ * @param bytesLeft The number of sample data bytes left to be read from the input.
+ * @return Whether the end of the sample data has been reached.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ boolean sampleData(ExtractorInput input, long bytesLeft)
+ throws IOException, InterruptedException;
+ }
+
+ private static final class PassthroughOutputWriter implements OutputWriter {
+
+ private final ExtractorOutput extractorOutput;
+ private final TrackOutput trackOutput;
+ private final WavHeader header;
+ private final Format format;
+ /** The target size of each output sample, in bytes. */
+ private final int targetSampleSizeBytes;
+
+ /** The time at which the writer was last {@link #reset}. */
+ private long startTimeUs;
+ /**
+ * The number of bytes that have been written to {@link #trackOutput} but have yet to be
+ * included as part of a sample (i.e. the corresponding call to {@link
+ * TrackOutput#sampleMetadata} has yet to be made).
+ */
+ private int pendingOutputBytes;
+ /**
+ * The total number of frames in samples that have been written to the trackOutput since the
+ * last call to {@link #reset}.
+ */
+ private long outputFrameCount;
+
+ public PassthroughOutputWriter(
+ ExtractorOutput extractorOutput,
+ TrackOutput trackOutput,
+ WavHeader header,
+ String mimeType,
+ @C.PcmEncoding int pcmEncoding)
+ throws ParserException {
+ this.extractorOutput = extractorOutput;
+ this.trackOutput = trackOutput;
+ this.header = header;
+
+ int bytesPerFrame = header.numChannels * header.bitsPerSample / 8;
+ // Validate the header. Blocks are expected to correspond to single frames.
+ if (header.blockSize != bytesPerFrame) {
+ throw new ParserException(
+ "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize);
+ }
+
+ targetSampleSizeBytes =
+ Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND);
+ format =
+ Format.createAudioSampleFormat(
+ /* id= */ null,
+ mimeType,
+ /* codecs= */ null,
+ /* bitrate= */ header.frameRateHz * bytesPerFrame * 8,
+ /* maxInputSize= */ targetSampleSizeBytes,
+ header.numChannels,
+ header.frameRateHz,
+ pcmEncoding,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null);
+ }
+
+ @Override
+ public void reset(long timeUs) {
+ startTimeUs = timeUs;
+ pendingOutputBytes = 0;
+ outputFrameCount = 0;
+ }
+
+ @Override
+ public void init(int dataStartPosition, long dataEndPosition) {
+ extractorOutput.seekMap(
+ new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition));
+ trackOutput.format(format);
+ }
+
+ @Override
+ public boolean sampleData(ExtractorInput input, long bytesLeft)
+ throws IOException, InterruptedException {
+ // Write sample data until we've reached the target sample size, or the end of the data.
+ while (bytesLeft > 0 && pendingOutputBytes < targetSampleSizeBytes) {
+ int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft);
+ int bytesAppended = trackOutput.sampleData(input, bytesToRead, true);
+ if (bytesAppended == RESULT_END_OF_INPUT) {
+ bytesLeft = 0;
+ } else {
+ pendingOutputBytes += bytesAppended;
+ bytesLeft -= bytesAppended;
+ }
+ }
+
+ // Write the corresponding sample metadata. Samples must be a whole number of frames. It's
+ // possible that the number of pending output bytes is not a whole number of frames if the
+ // stream ended unexpectedly.
+ int bytesPerFrame = header.blockSize;
+ int pendingFrames = pendingOutputBytes / bytesPerFrame;
+ if (pendingFrames > 0) {
+ long timeUs =
+ startTimeUs
+ + Util.scaleLargeTimestamp(
+ outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz);
+ int size = pendingFrames * bytesPerFrame;
+ int offset = pendingOutputBytes - size;
+ trackOutput.sampleMetadata(
+ timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null);
+ outputFrameCount += pendingFrames;
+ pendingOutputBytes = offset;
+ }
+
+ return bytesLeft <= 0;
+ }
+ }
+
+ private static final class ImaAdPcmOutputWriter implements OutputWriter {
+
+ private static final int[] INDEX_TABLE = {
+ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8
+ };
+
+ private static final int[] STEP_TABLE = {
+ 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66,
+ 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408,
+ 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066,
+ 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630,
+ 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794,
+ 32767
+ };
+
+ private final ExtractorOutput extractorOutput;
+ private final TrackOutput trackOutput;
+ private final WavHeader header;
+
+ /** Number of frames per block of the input (yet to be decoded) data. */
+ private final int framesPerBlock;
+ /** Target for the input (yet to be decoded) data. */
+ private final byte[] inputData;
+ /** Target for decoded (yet to be output) data. */
+ private final ParsableByteArray decodedData;
+ /** The target size of each output sample, in frames. */
+ private final int targetSampleSizeFrames;
+ /** The output format. */
+ private final Format format;
+
+ /** The number of pending bytes in {@link #inputData}. */
+ private int pendingInputBytes;
+ /** The time at which the writer was last {@link #reset}. */
+ private long startTimeUs;
+ /**
+ * The number of bytes that have been written to {@link #trackOutput} but have yet to be
+ * included as part of a sample (i.e. the corresponding call to {@link
+ * TrackOutput#sampleMetadata} has yet to be made).
+ */
+ private int pendingOutputBytes;
+ /**
+ * The total number of frames in samples that have been written to the trackOutput since the
+ * last call to {@link #reset}.
+ */
+ private long outputFrameCount;
+
+ public ImaAdPcmOutputWriter(
+ ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header)
+ throws ParserException {
+ this.extractorOutput = extractorOutput;
+ this.trackOutput = trackOutput;
+ this.header = header;
+ targetSampleSizeFrames = Math.max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND);
+
+ ParsableByteArray scratch = new ParsableByteArray(header.extraData);
+ scratch.readLittleEndianUnsignedShort();
+ framesPerBlock = scratch.readLittleEndianUnsignedShort();
+
+ int numChannels = header.numChannels;
+ // Validate the header. This calculation is defined in "Microsoft Multimedia Standards Update
+ // - New Multimedia Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and "DVI
+ // ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter.
+ int expectedFramesPerBlock =
+ (((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1;
+ if (framesPerBlock != expectedFramesPerBlock) {
+ throw new ParserException(
+ "Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock);
+ }
+
+ // Calculate the number of blocks we'll need to decode to obtain an output sample of the
+ // target sample size, and allocate suitably sized buffers for input and decoded data.
+ int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock);
+ inputData = new byte[maxBlocksToDecode * header.blockSize];
+ decodedData =
+ new ParsableByteArray(
+ maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels));
+
+ // Create the format. We calculate the bitrate of the data before decoding, since this is the
+ // bitrate of the stream itself.
+ int bitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock;
+ format =
+ Format.createAudioSampleFormat(
+ /* id= */ null,
+ MimeTypes.AUDIO_RAW,
+ /* codecs= */ null,
+ bitrate,
+ /* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames, numChannels),
+ header.numChannels,
+ header.frameRateHz,
+ C.ENCODING_PCM_16BIT,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null);
+ }
+
+ @Override
+ public void reset(long timeUs) {
+ pendingInputBytes = 0;
+ startTimeUs = timeUs;
+ pendingOutputBytes = 0;
+ outputFrameCount = 0;
+ }
+
+ @Override
+ public void init(int dataStartPosition, long dataEndPosition) {
+ extractorOutput.seekMap(
+ new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition));
+ trackOutput.format(format);
+ }
+
+ @Override
+ public boolean sampleData(ExtractorInput input, long bytesLeft)
+ throws IOException, InterruptedException {
+ // Calculate the number of additional frames that we need on the output side to complete a
+ // sample of the target size.
+ int targetFramesRemaining =
+ targetSampleSizeFrames - numOutputBytesToFrames(pendingOutputBytes);
+ // Calculate the whole number of blocks that we need to decode to obtain this many frames.
+ int blocksToDecode = Util.ceilDivide(targetFramesRemaining, framesPerBlock);
+ int targetReadBytes = blocksToDecode * header.blockSize;
+
+ // Read input data until we've reached the target number of blocks, or the end of the data.
+ boolean endOfSampleData = bytesLeft == 0;
+ while (!endOfSampleData && pendingInputBytes < targetReadBytes) {
+ int bytesToRead = (int) Math.min(targetReadBytes - pendingInputBytes, bytesLeft);
+ int bytesAppended = input.read(inputData, pendingInputBytes, bytesToRead);
+ if (bytesAppended == RESULT_END_OF_INPUT) {
+ endOfSampleData = true;
+ } else {
+ pendingInputBytes += bytesAppended;
+ }
+ }
+
+ int pendingBlockCount = pendingInputBytes / header.blockSize;
+ if (pendingBlockCount > 0) {
+ // We have at least one whole block to decode.
+ decode(inputData, pendingBlockCount, decodedData);
+ pendingInputBytes -= pendingBlockCount * header.blockSize;
+
+ // Write all of the decoded data to the track output.
+ int decodedDataSize = decodedData.limit();
+ trackOutput.sampleData(decodedData, decodedDataSize);
+ pendingOutputBytes += decodedDataSize;
+
+ // Output the next sample at the target size.
+ int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes);
+ if (pendingOutputFrames >= targetSampleSizeFrames) {
+ writeSampleMetadata(targetSampleSizeFrames);
+ }
+ }
+
+ // If we've reached the end of the data, we might need to output a final partial sample.
+ if (endOfSampleData) {
+ int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes);
+ if (pendingOutputFrames > 0) {
+ writeSampleMetadata(pendingOutputFrames);
+ }
+ }
+
+ return endOfSampleData;
+ }
+
+ private void writeSampleMetadata(int sampleFrames) {
+ long timeUs =
+ startTimeUs
+ + Util.scaleLargeTimestamp(outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz);
+ int size = numOutputFramesToBytes(sampleFrames);
+ int offset = pendingOutputBytes - size;
+ trackOutput.sampleMetadata(
+ timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null);
+ outputFrameCount += sampleFrames;
+ pendingOutputBytes -= size;
+ }
+
+ /**
+ * Decodes IMA ADPCM data to 16 bit PCM.
+ *
+ * @param input The input data to decode.
+ * @param blockCount The number of blocks to decode.
+ * @param output The output into which the decoded data will be written.
+ */
+ private void decode(byte[] input, int blockCount, ParsableByteArray output) {
+ for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) {
+ for (int channelIndex = 0; channelIndex < header.numChannels; channelIndex++) {
+ decodeBlockForChannel(input, blockIndex, channelIndex, output.data);
+ }
+ }
+ int decodedDataSize = numOutputFramesToBytes(framesPerBlock * blockCount);
+ output.reset(decodedDataSize);
+ }
+
+ private void decodeBlockForChannel(
+ byte[] input, int blockIndex, int channelIndex, byte[] output) {
+ int blockSize = header.blockSize;
+ int numChannels = header.numChannels;
+
+ // The input data consists for a four byte header [Ci] for each of the N channels, followed
+ // by interleaved data segments [Ci-DATAj], each of which are four bytes long.
+ //
+ // [C1][C2]...[CN] [C1-Data0][C2-Data0]...[CN-Data0] [C1-Data1][C2-Data1]...[CN-Data1] etc
+ //
+ // Compute the start indices for the [Ci] and [Ci-Data0] for the current channel, as well as
+ // the number of data bytes for the channel in the block.
+ int blockStartIndex = blockIndex * blockSize;
+ int headerStartIndex = blockStartIndex + channelIndex * 4;
+ int dataStartIndex = headerStartIndex + numChannels * 4;
+ int dataSizeBytes = blockSize / numChannels - 4;
+
+ // Decode initialization. Casting to a short is necessary for the most significant bit to be
+ // treated as -2^15 rather than 2^15.
+ int predictedSample =
+ (short) (((input[headerStartIndex + 1] & 0xFF) << 8) | (input[headerStartIndex] & 0xFF));
+ int stepIndex = Math.min(input[headerStartIndex + 2] & 0xFF, 88);
+ int step = STEP_TABLE[stepIndex];
+
+ // Output the initial 16 bit PCM sample from the header.
+ int outputIndex = (blockIndex * framesPerBlock * numChannels + channelIndex) * 2;
+ output[outputIndex] = (byte) (predictedSample & 0xFF);
+ output[outputIndex + 1] = (byte) (predictedSample >> 8);
+
+ // We examine each data byte twice during decode.
+ for (int i = 0; i < dataSizeBytes * 2; i++) {
+ int dataSegmentIndex = i / 8;
+ int dataSegmentOffset = (i / 2) % 4;
+ int dataIndex = dataStartIndex + (dataSegmentIndex * numChannels * 4) + dataSegmentOffset;
+
+ int originalSample = input[dataIndex] & 0xFF;
+ if (i % 2 == 0) {
+ originalSample &= 0x0F; // Bottom four bits.
+ } else {
+ originalSample >>= 4; // Top four bits.
+ }
+
+ int delta = originalSample & 0x07;
+ int difference = ((2 * delta + 1) * step) >> 3;
+
+ if ((originalSample & 0x08) != 0) {
+ difference = -difference;
+ }
+
+ predictedSample += difference;
+ predictedSample = Util.constrainValue(predictedSample, /* min= */ -32768, /* max= */ 32767);
+
+ // Output the next 16 bit PCM sample to the correct position in the output.
+ outputIndex += 2 * numChannels;
+ output[outputIndex] = (byte) (predictedSample & 0xFF);
+ output[outputIndex + 1] = (byte) (predictedSample >> 8);
+
+ stepIndex += INDEX_TABLE[originalSample];
+ stepIndex = Util.constrainValue(stepIndex, /* min= */ 0, /* max= */ STEP_TABLE.length - 1);
+ step = STEP_TABLE[stepIndex];
+ }
+ }
+
+ private int numOutputBytesToFrames(int bytes) {
+ return bytes / (2 * header.numChannels);
+ }
+
+ private int numOutputFramesToBytes(int frames) {
+ return numOutputFramesToBytes(frames, header.numChannels);
+ }
+
+ private static int numOutputFramesToBytes(int frames, int numChannels) {
+ return frames * 2 * numChannels;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java
new file mode 100644
index 0000000000..bc6cf8999b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java
@@ -0,0 +1,55 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav;
+
+/** Header for a WAV file. */
+/* package */ final class WavHeader {
+
+ /**
+ * The format type. Standard format types are the "WAVE form Registration Number" constants
+ * defined in RFC 2361 Appendix A.
+ */
+ public final int formatType;
+ /** The number of channels. */
+ public final int numChannels;
+ /** The sample rate in Hertz. */
+ public final int frameRateHz;
+ /** The average bytes per second for the sample data. */
+ public final int averageBytesPerSecond;
+ /** The block size in bytes. */
+ public final int blockSize;
+ /** Bits per sample for a single channel. */
+ public final int bitsPerSample;
+ /** Extra data appended to the format chunk of the header. */
+ public final byte[] extraData;
+
+ public WavHeader(
+ int formatType,
+ int numChannels,
+ int frameRateHz,
+ int averageBytesPerSecond,
+ int blockSize,
+ int bitsPerSample,
+ byte[] extraData) {
+ this.formatType = formatType;
+ this.numChannels = numChannels;
+ this.frameRateHz = frameRateHz;
+ this.averageBytesPerSecond = averageBytesPerSecond;
+ this.blockSize = blockSize;
+ this.bitsPerSample = bitsPerSample;
+ this.extraData = extraData;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
new file mode 100644
index 0000000000..1c36aaa3c3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
@@ -0,0 +1,191 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav;
+
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.WavUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */
+/* package */ final class WavHeaderReader {
+
+ private static final String TAG = "WavHeaderReader";
+
+ /**
+ * Peeks and returns a {@code WavHeader}.
+ *
+ * @param input Input stream to peek the WAV header from.
+ * @throws ParserException If the input file is an incorrect RIFF WAV.
+ * @throws IOException If peeking from the input fails.
+ * @throws InterruptedException If interrupted while peeking from input.
+ * @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a
+ * supported WAV format.
+ */
+ @Nullable
+ public static WavHeader peek(ExtractorInput input) throws IOException, InterruptedException {
+ Assertions.checkNotNull(input);
+
+ // Allocate a scratch buffer large enough to store the format chunk.
+ ParsableByteArray scratch = new ParsableByteArray(16);
+
+ // Attempt to read the RIFF chunk.
+ ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
+ if (chunkHeader.id != WavUtil.RIFF_FOURCC) {
+ return null;
+ }
+
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+ int riffFormat = scratch.readInt();
+ if (riffFormat != WavUtil.WAVE_FOURCC) {
+ Log.e(TAG, "Unsupported RIFF format: " + riffFormat);
+ return null;
+ }
+
+ // Skip chunks until we find the format chunk.
+ chunkHeader = ChunkHeader.peek(input, scratch);
+ while (chunkHeader.id != WavUtil.FMT_FOURCC) {
+ input.advancePeekPosition((int) chunkHeader.size);
+ chunkHeader = ChunkHeader.peek(input, scratch);
+ }
+
+ Assertions.checkState(chunkHeader.size >= 16);
+ input.peekFully(scratch.data, 0, 16);
+ scratch.setPosition(0);
+ int audioFormatType = scratch.readLittleEndianUnsignedShort();
+ int numChannels = scratch.readLittleEndianUnsignedShort();
+ int frameRateHz = scratch.readLittleEndianUnsignedIntToInt();
+ int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt();
+ int blockSize = scratch.readLittleEndianUnsignedShort();
+ int bitsPerSample = scratch.readLittleEndianUnsignedShort();
+
+ int bytesLeft = (int) chunkHeader.size - 16;
+ byte[] extraData;
+ if (bytesLeft > 0) {
+ extraData = new byte[bytesLeft];
+ input.peekFully(extraData, 0, bytesLeft);
+ } else {
+ extraData = Util.EMPTY_BYTE_ARRAY;
+ }
+
+ return new WavHeader(
+ audioFormatType,
+ numChannels,
+ frameRateHz,
+ averageBytesPerSecond,
+ blockSize,
+ bitsPerSample,
+ extraData);
+ }
+
+ /**
+ * Skips to the data in the given WAV input stream, and returns its bounds. After calling, the
+ * input stream's position will point to the start of sample data in the WAV. If an exception is
+ * thrown, the input position will be left pointing to a chunk header.
+ *
+ * @param input The input stream, whose read position must be pointing to a valid chunk header.
+ * @return The byte positions at which the data starts (inclusive) and ends (exclusive).
+ * @throws ParserException If an error occurs parsing chunks.
+ * @throws IOException If reading from the input fails.
+ * @throws InterruptedException If interrupted while reading from input.
+ */
+ public static Pair<Long, Long> skipToData(ExtractorInput input)
+ throws IOException, InterruptedException {
+ Assertions.checkNotNull(input);
+
+ // Make sure the peek position is set to the read position before we peek the first header.
+ input.resetPeekPosition();
+
+ ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES);
+ // Skip all chunks until we hit the data header.
+ ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
+ while (chunkHeader.id != WavUtil.DATA_FOURCC) {
+ if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) {
+ Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id);
+ }
+ long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size;
+ // Override size of RIFF chunk, since it describes its size as the entire file.
+ if (chunkHeader.id == WavUtil.RIFF_FOURCC) {
+ bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4;
+ }
+ if (bytesToSkip > Integer.MAX_VALUE) {
+ throw new ParserException("Chunk is too large (~2GB+) to skip; id: " + chunkHeader.id);
+ }
+ input.skipFully((int) bytesToSkip);
+ chunkHeader = ChunkHeader.peek(input, scratch);
+ }
+ // Skip past the "data" header.
+ input.skipFully(ChunkHeader.SIZE_IN_BYTES);
+
+ long dataStartPosition = input.getPosition();
+ long dataEndPosition = dataStartPosition + chunkHeader.size;
+ long inputLength = input.getLength();
+ if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) {
+ Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength);
+ dataEndPosition = inputLength;
+ }
+ return Pair.create(dataStartPosition, dataEndPosition);
+ }
+
+ private WavHeaderReader() {
+ // Prevent instantiation.
+ }
+
+ /** Container for a WAV chunk header. */
+ private static final class ChunkHeader {
+
+ /** Size in bytes of a WAV chunk header. */
+ public static final int SIZE_IN_BYTES = 8;
+
+ /** 4-character identifier, stored as an integer, for this chunk. */
+ public final int id;
+ /** Size of this chunk in bytes. */
+ public final long size;
+
+ private ChunkHeader(int id, long size) {
+ this.id = id;
+ this.size = size;
+ }
+
+ /**
+ * Peeks and returns a {@link ChunkHeader}.
+ *
+ * @param input Input stream to peek the chunk header from.
+ * @param scratch Buffer for temporary use.
+ * @throws IOException If peeking from the input fails.
+ * @throws InterruptedException If interrupted while peeking from input.
+ * @return A new {@code ChunkHeader} peeked from {@code input}.
+ */
+ public static ChunkHeader peek(ExtractorInput input, ParsableByteArray scratch)
+ throws IOException, InterruptedException {
+ input.peekFully(scratch.data, /* offset= */ 0, /* length= */ SIZE_IN_BYTES);
+ scratch.setPosition(0);
+
+ int id = scratch.readInt();
+ long size = scratch.readLittleEndianUnsignedInt();
+
+ return new ChunkHeader(id, size);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java
new file mode 100644
index 0000000000..d14268d120
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/* package */ final class WavSeekMap implements SeekMap {
+
+ private final WavHeader wavHeader;
+ private final int framesPerBlock;
+ private final long firstBlockPosition;
+ private final long blockCount;
+ private final long durationUs;
+
+ public WavSeekMap(
+ WavHeader wavHeader, int framesPerBlock, long dataStartPosition, long dataEndPosition) {
+ this.wavHeader = wavHeader;
+ this.framesPerBlock = framesPerBlock;
+ this.firstBlockPosition = dataStartPosition;
+ this.blockCount = (dataEndPosition - dataStartPosition) / wavHeader.blockSize;
+ durationUs = blockIndexToTimeUs(blockCount);
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ // Calculate the containing block index, constraining to valid indices.
+ long blockIndex = (timeUs * wavHeader.frameRateHz) / (C.MICROS_PER_SECOND * framesPerBlock);
+ blockIndex = Util.constrainValue(blockIndex, 0, blockCount - 1);
+
+ long seekPosition = firstBlockPosition + (blockIndex * wavHeader.blockSize);
+ long seekTimeUs = blockIndexToTimeUs(blockIndex);
+ SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
+ if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) {
+ return new SeekPoints(seekPoint);
+ } else {
+ long secondBlockIndex = blockIndex + 1;
+ long secondSeekPosition = firstBlockPosition + (secondBlockIndex * wavHeader.blockSize);
+ long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex);
+ SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
+ return new SeekPoints(seekPoint, secondSeekPoint);
+ }
+ }
+
+ private long blockIndexToTimeUs(long blockIndex) {
+ return Util.scaleLargeTimestamp(
+ blockIndex * framesPerBlock, C.MICROS_PER_SECOND, wavHeader.frameRateHz);
+ }
+}