diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java | 332 |
1 files changed, 332 insertions, 0 deletions
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); + } +} |