summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts')
-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
31 files changed, 7271 insertions, 0 deletions
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);
+ }
+ }
+}