blob: 6ebf9d31b530a30e54ebb8c4f7d2e92a9079e62b [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH;
import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG;
import androidx.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.EOFException;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** 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 values are {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} and {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS})
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;
/**
* Like {@link #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}, except that seeking is also enabled in
* cases where the content length (and hence the duration of the media) is unknown. Application
* code should ensure that requested seek positions are valid when using this flag, or be ready to
* handle playback failures reported through {@link Player.Listener#onPlayerError} with {@link
* PlaybackException#errorCode} set to {@link
* PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE}.
*
* <p>If this flag is set, then the behavior enabled by {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} is implicitly enabled as well.
*/
public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS = 1 << 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;
private @MonotonicNonNull 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) {
if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0) {
flags |= FLAG_ENABLE_CONSTANT_BITRATE_SEEKING;
}
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.getData());
}
// Extractor implementation.
@Override
public boolean sniff(ExtractorInput input) throws IOException {
// 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.getData(), 0, 2);
scratch.setPosition(0);
int syncBytes = scratch.readUnsignedShort();
if (!AdtsReader.isAdtsSyncWord(syncBytes)) {
// We didn't find an ADTS sync word. Start searching again from one byte further into the
// start of the stream.
validFramesCount = 0;
totalValidFramesSize = 0;
headerPosition++;
input.resetPeekPosition();
input.advancePeekPosition(headerPosition);
} else {
if (++validFramesCount >= 4 && totalValidFramesSize > TsExtractor.TS_PACKET_SIZE) {
return true;
}
// Skip the frame.
input.peekFully(scratch.getData(), 0, 4);
scratchBits.setPosition(14);
int frameSize = scratchBits.readBits(13);
if (frameSize <= 6) {
// The size is too small, so we're probably not reading an ADTS frame. Start searching
// again from one byte further into the start of the stream.
validFramesCount = 0;
totalValidFramesSize = 0;
headerPosition++;
input.resetPeekPosition();
input.advancePeekPosition(headerPosition);
} else {
input.advancePeekPosition(frameSize - 6);
totalValidFramesSize += frameSize;
}
}
if (headerPosition - startPosition >= MAX_SNIFF_BYTES) {
return false;
}
}
}
@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 {
Assertions.checkStateNotNull(extractorOutput); // Asserts that init has been called.
long inputLength = input.getLength();
boolean canUseConstantBitrateSeeking =
(flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0
|| ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0
&& inputLength != C.LENGTH_UNSET);
if (canUseConstantBitrateSeeking) {
calculateAverageFrameSize(input);
}
int bytesRead = input.read(packetBuffer.getData(), 0, MAX_PACKET_SIZE);
boolean readEndOfStream = bytesRead == RESULT_END_OF_INPUT;
maybeOutputSeekMap(inputLength, 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 {
int firstFramePosition = 0;
while (true) {
input.peekFully(scratch.getData(), /* 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;
}
@RequiresNonNull("extractorOutput")
private void maybeOutputSeekMap(long inputLength, boolean readEndOfStream) {
if (hasOutputSeekMap) {
return;
}
boolean useConstantBitrateSeeking =
(flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0 && 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;
}
if (useConstantBitrateSeeking && reader.getSampleDurationUs() != C.TIME_UNSET) {
extractorOutput.seekMap(
getConstantBitrateSeekMap(
inputLength, (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0));
} else {
extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
}
hasOutputSeekMap = true;
}
private void calculateAverageFrameSize(ExtractorInput input) throws IOException {
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.getData(), /* 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.getData(), /* 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 ParserException.createForMalformedContainer(
"Malformed ADTS stream", /* cause= */ null);
}
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, boolean allowSeeksIfLengthUnknown) {
int bitrate = getBitrateFromFrameSize(averageFrameSize, reader.getSampleDurationUs());
return new ConstantBitrateSeekMap(
inputLength, firstFramePosition, bitrate, averageFrameSize, allowSeeksIfLengthUnknown);
}
/**
* 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);
}
}