diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts')
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); + } + } +} |