diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java index 44a15f5746..a0dab7d60d 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java @@ -161,6 +161,13 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu } catch (ClassNotFoundException e) { // Extractor not found. } + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("com.google.android.exoplayer.extractor.ts.PsExtractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } } private final ExtractorHolder extractorHolder; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/PsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/PsExtractor.java new file mode 100644 index 0000000000..c07a7ba08a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/PsExtractor.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.extractor.ts; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.extractor.Extractor; +import com.google.android.exoplayer.extractor.ExtractorInput; +import com.google.android.exoplayer.extractor.ExtractorOutput; +import com.google.android.exoplayer.extractor.PositionHolder; +import com.google.android.exoplayer.extractor.SeekMap; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; + +import android.util.Log; +import android.util.SparseArray; + +import java.io.IOException; + +/** + * Facilitates the extraction of data from the MPEG-2 TS container format. + */ +public final class PsExtractor implements Extractor { + + private static final String TAG = "PsExtractor"; + + private static final int PACK_START_CODE = 0x000001BA; + private static final int SYSTEM_HEADER_START_CODE = 0x000001BB; + private static final int PACKET_START_CODE_PREFIX = 0x000001; + private static final int MPEG_PROGRAM_END_CODE = 0x000001B9; + private static final long MAX_SEARCH_LENGTH = 1024 * 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 PtsTimestampAdjuster ptsTimestampAdjuster; + private final SparseArray psPayloadReaders; // Indexed by pid + private final ParsableByteArray psPacketBuffer; + private boolean foundAllTracks; + private boolean foundAudioTrack; + private boolean foundVideoTrack; + + // Accessed only by the loading thread. + private ExtractorOutput output; + + public PsExtractor() { + this(new PtsTimestampAdjuster(0)); + } + + public PsExtractor(PtsTimestampAdjuster ptsTimestampAdjuster) { + this.ptsTimestampAdjuster = ptsTimestampAdjuster; + psPacketBuffer = new ParsableByteArray(4096); + psPayloadReaders = new SparseArray<>(); + } + + // 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; + output.seekMap(new SeekMap.Unseekable(C.UNKNOWN_TIME_US)); + } + + @Override + public void seek() { + ptsTimestampAdjuster.reset(); + for (int i = 0; i < psPayloadReaders.size(); i++) { + psPayloadReaders.valueAt(i).seek(); + } + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + // 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(0); + psPacketBuffer.skipBytes(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) { + Log.w(TAG, "Missing 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) { + if (streamId == PRIVATE_STREAM_1 && !foundAudioTrack) { + // 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. + payloadReader = new PesReader(new Ac3Reader(output.track(streamId), false)); + psPayloadReaders.put(streamId, payloadReader); + foundAudioTrack = true; + Log.d(TAG, "Setup payload reader for AC3"); + } else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM && !foundAudioTrack) { + payloadReader = new PesReader(new MpegAudioReader(output.track(streamId))); + psPayloadReaders.put(streamId, payloadReader); + foundAudioTrack = true; + Log.d(TAG, "Setup payload reader for MP2"); + } else if ((streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM && !foundVideoTrack) { + payloadReader = new PesReader(new H262Reader(output.track(streamId))); + psPayloadReaders.put(streamId, payloadReader); + foundVideoTrack = true; + Log.d(TAG, "Setup payload reader for MPEG2Video"); + } + } + if ((foundAudioTrack && foundVideoTrack) || input.getPosition() > MAX_SEARCH_LENGTH) { + foundAllTracks = true; + output.endTracks(); + Log.d(TAG, "Signalled that all tracks were found"); + } + } + + // 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 { + if (psPacketBuffer.capacity() < pesLength) { + // Reallocate for this and future packets. + psPacketBuffer.reset(new byte[pesLength], pesLength); + } + // Read the whole packet and the header for consumption. + input.readFully(psPacketBuffer.data, 0, pesLength); + psPacketBuffer.setPosition(6); + psPacketBuffer.setLimit(pesLength); + payloadReader.consume(psPacketBuffer, output); + psPacketBuffer.setLimit(psPacketBuffer.capacity()); + } + + return RESULT_CONTINUE; + } + + // Internals. + + /** + * Parses PES packet data and extracts samples. + */ + private class PesReader { + + private static final int PES_SCRATCH_SIZE = 64; + + private final ParsableBitArray pesScratch; + private final ElementaryStreamReader pesPayloadReader; + + private boolean ptsFlag; + private boolean dtsFlag; + private boolean seenFirstDts; + private int extendedHeaderLength; + private long timeUs; + + public PesReader(ElementaryStreamReader pesPayloadReader) { + this.pesPayloadReader = pesPayloadReader; + pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]); + } + + /** + * Notifies the reader that a seek has occurred. + *

+ * Following a call to this method, the data passed to the next invocation of + * {@link #consume(ParsableByteArray, ExtractorOutput)} 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. + * @param output The output to which parsed data should be written. + */ + public void consume(ParsableByteArray data, ExtractorOutput output) { + data.readBytes(pesScratch.data, 0, 3); + pesScratch.setPosition(0); + parseHeader(); + data.readBytes(pesScratch.data, 0, extendedHeaderLength); + pesScratch.setPosition(0); + parseHeaderExtension(); + pesPayloadReader.packetStarted(timeUs, true); + 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. + ptsTimestampAdjuster.adjustTimestamp(dts); + seenFirstDts = true; + } + timeUs = ptsTimestampAdjuster.adjustTimestamp(pts); + } + } + + } + +}