| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.google.android.exoplayer2.extractor.ts; |
| |
| import android.util.SparseArray; |
| import androidx.annotation.Nullable; |
| import com.google.android.exoplayer2.C; |
| import com.google.android.exoplayer2.ParserException; |
| import com.google.android.exoplayer2.extractor.Extractor; |
| import com.google.android.exoplayer2.extractor.ExtractorInput; |
| import com.google.android.exoplayer2.extractor.ExtractorOutput; |
| import com.google.android.exoplayer2.extractor.ExtractorsFactory; |
| import com.google.android.exoplayer2.extractor.PositionHolder; |
| import com.google.android.exoplayer2.extractor.SeekMap; |
| import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; |
| import com.google.android.exoplayer2.util.Assertions; |
| import com.google.android.exoplayer2.util.ParsableBitArray; |
| import com.google.android.exoplayer2.util.ParsableByteArray; |
| import com.google.android.exoplayer2.util.TimestampAdjuster; |
| import java.io.IOException; |
| import org.checkerframework.checker.nullness.qual.MonotonicNonNull; |
| import org.checkerframework.checker.nullness.qual.RequiresNonNull; |
| |
| /** |
| * 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. |
| @Nullable private PsBinarySearchSeeker psBinarySearchSeeker; |
| private @MonotonicNonNull 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 { |
| 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 { |
| Assertions.checkStateNotNull(output); // Asserts init has been called. |
| |
| 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) { |
| @Nullable 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. |
| |
| @RequiresNonNull("output") |
| 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); |
| } |
| } |
| |
| } |
| |
| } |