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