blob: 3c870616fd068c91a55b081db48cbc4d1ee2d23a [file] [log] [blame]
/*
* 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 com.google.android.exoplayer2.extractor.flac;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
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.FlacFrameReader;
import com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder;
import com.google.android.exoplayer2.extractor.FlacMetadataReader;
import com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap;
import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray;
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;
/**
* Extracts data from FLAC container format.
*
* <p>The format specification can be found at https://xiph.org/flac/format.html.
*/
public final class FlacExtractor implements Extractor {
/** Factory for {@link FlacExtractor} instances. */
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};
/*
* Flags in the two FLAC extractors should be kept in sync. If we ever change this then
* DefaultExtractorsFactory will need modifying, because it currently assumes this is the case.
*/
/**
* Flags controlling the behavior of the extractor. Possible flag value is {@link
* #FLAG_DISABLE_ID3_METADATA}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {FLAG_DISABLE_ID3_METADATA})
public @interface Flags {}
/**
* Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
* required.
*/
public static final int FLAG_DISABLE_ID3_METADATA = 1;
/** Parser state. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STATE_READ_ID3_METADATA,
STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES,
STATE_READ_STREAM_MARKER,
STATE_READ_METADATA_BLOCKS,
STATE_GET_FRAME_START_MARKER,
STATE_READ_FRAMES
})
private @interface State {}
private static final int STATE_READ_ID3_METADATA = 0;
private static final int STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES = 1;
private static final int STATE_READ_STREAM_MARKER = 2;
private static final int STATE_READ_METADATA_BLOCKS = 3;
private static final int STATE_GET_FRAME_START_MARKER = 4;
private static final int STATE_READ_FRAMES = 5;
/** Arbitrary buffer length of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */
private static final int BUFFER_LENGTH = 32 * 1024;
/** Value of an unknown sample number. */
private static final int SAMPLE_NUMBER_UNKNOWN = -1;
private final byte[] streamMarkerAndInfoBlock;
private final ParsableByteArray buffer;
private final boolean id3MetadataDisabled;
private final SampleNumberHolder sampleNumberHolder;
private @MonotonicNonNull ExtractorOutput extractorOutput;
private @MonotonicNonNull TrackOutput trackOutput;
private @State int state;
@Nullable private Metadata id3Metadata;
private @MonotonicNonNull FlacStreamMetadata flacStreamMetadata;
private int minFrameSize;
private int frameStartMarker;
private @MonotonicNonNull FlacBinarySearchSeeker binarySearchSeeker;
private int currentFrameBytesWritten;
private long currentFrameFirstSampleNumber;
/** Constructs an instance with {@code flags = 0}. */
public FlacExtractor() {
this(/* flags= */ 0);
}
/**
* Constructs an instance.
*
* @param flags Flags that control the extractor's behavior. Possible flags are described by
* {@link Flags}.
*/
public FlacExtractor(int flags) {
streamMarkerAndInfoBlock =
new byte[FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE];
buffer = new ParsableByteArray(new byte[BUFFER_LENGTH], /* limit= */ 0);
id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
sampleNumberHolder = new SampleNumberHolder();
state = STATE_READ_ID3_METADATA;
}
@Override
public boolean sniff(ExtractorInput input) throws IOException {
FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false);
return FlacMetadataReader.checkAndPeekStreamMarker(input);
}
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);
output.endTracks();
}
@Override
public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException {
switch (state) {
case STATE_READ_ID3_METADATA:
readId3Metadata(input);
return Extractor.RESULT_CONTINUE;
case STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES:
getStreamMarkerAndInfoBlockBytes(input);
return Extractor.RESULT_CONTINUE;
case STATE_READ_STREAM_MARKER:
readStreamMarker(input);
return Extractor.RESULT_CONTINUE;
case STATE_READ_METADATA_BLOCKS:
readMetadataBlocks(input);
return Extractor.RESULT_CONTINUE;
case STATE_GET_FRAME_START_MARKER:
getFrameStartMarker(input);
return Extractor.RESULT_CONTINUE;
case STATE_READ_FRAMES:
return readFrames(input, seekPosition);
default:
throw new IllegalStateException();
}
}
@Override
public void seek(long position, long timeUs) {
if (position == 0) {
state = STATE_READ_ID3_METADATA;
} else if (binarySearchSeeker != null) {
binarySearchSeeker.setSeekTargetUs(timeUs);
}
currentFrameFirstSampleNumber = timeUs == 0 ? 0 : SAMPLE_NUMBER_UNKNOWN;
currentFrameBytesWritten = 0;
buffer.reset(/* limit= */ 0);
}
@Override
public void release() {
// Do nothing.
}
// Private methods.
private void readId3Metadata(ExtractorInput input) throws IOException {
id3Metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ !id3MetadataDisabled);
state = STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES;
}
private void getStreamMarkerAndInfoBlockBytes(ExtractorInput input) throws IOException {
input.peekFully(streamMarkerAndInfoBlock, 0, streamMarkerAndInfoBlock.length);
input.resetPeekPosition();
state = STATE_READ_STREAM_MARKER;
}
private void readStreamMarker(ExtractorInput input) throws IOException {
FlacMetadataReader.readStreamMarker(input);
state = STATE_READ_METADATA_BLOCKS;
}
private void readMetadataBlocks(ExtractorInput input) throws IOException {
boolean isLastMetadataBlock = false;
FlacMetadataReader.FlacStreamMetadataHolder metadataHolder =
new FlacMetadataReader.FlacStreamMetadataHolder(flacStreamMetadata);
while (!isLastMetadataBlock) {
isLastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, metadataHolder);
// Save the current metadata in case an exception occurs.
flacStreamMetadata = castNonNull(metadataHolder.flacStreamMetadata);
}
Assertions.checkNotNull(flacStreamMetadata);
minFrameSize = max(flacStreamMetadata.minFrameSize, FlacConstants.MIN_FRAME_HEADER_SIZE);
castNonNull(trackOutput)
.format(flacStreamMetadata.getFormat(streamMarkerAndInfoBlock, id3Metadata));
state = STATE_GET_FRAME_START_MARKER;
}
private void getFrameStartMarker(ExtractorInput input) throws IOException {
frameStartMarker = FlacMetadataReader.getFrameStartMarker(input);
castNonNull(extractorOutput)
.seekMap(
getSeekMap(
/* firstFramePosition= */ input.getPosition(),
/* streamLength= */ input.getLength()));
state = STATE_READ_FRAMES;
}
private @ReadResult int readFrames(ExtractorInput input, PositionHolder seekPosition)
throws IOException {
Assertions.checkNotNull(trackOutput);
Assertions.checkNotNull(flacStreamMetadata);
// Handle pending binary search seek if necessary.
if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
return binarySearchSeeker.handlePendingSeek(input, seekPosition);
}
// Set current frame first sample number if it became unknown after seeking.
if (currentFrameFirstSampleNumber == SAMPLE_NUMBER_UNKNOWN) {
currentFrameFirstSampleNumber =
FlacFrameReader.getFirstSampleNumber(input, flacStreamMetadata);
return Extractor.RESULT_CONTINUE;
}
// Copy more bytes into the buffer.
int currentLimit = buffer.limit();
boolean foundEndOfInput = false;
if (currentLimit < BUFFER_LENGTH) {
int bytesRead =
input.read(
buffer.getData(),
/* offset= */ currentLimit,
/* length= */ BUFFER_LENGTH - currentLimit);
foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT;
if (!foundEndOfInput) {
buffer.setLimit(currentLimit + bytesRead);
} else if (buffer.bytesLeft() == 0) {
outputSampleMetadata();
return Extractor.RESULT_END_OF_INPUT;
}
}
// Search for a frame.
int positionBeforeFindingAFrame = buffer.getPosition();
// Skip frame search on the bytes within the minimum frame size.
if (currentFrameBytesWritten < minFrameSize) {
buffer.skipBytes(min(minFrameSize - currentFrameBytesWritten, buffer.bytesLeft()));
}
long nextFrameFirstSampleNumber = findFrame(buffer, foundEndOfInput);
int numberOfFrameBytes = buffer.getPosition() - positionBeforeFindingAFrame;
buffer.setPosition(positionBeforeFindingAFrame);
trackOutput.sampleData(buffer, numberOfFrameBytes);
currentFrameBytesWritten += numberOfFrameBytes;
// Frame found.
if (nextFrameFirstSampleNumber != SAMPLE_NUMBER_UNKNOWN) {
outputSampleMetadata();
currentFrameBytesWritten = 0;
currentFrameFirstSampleNumber = nextFrameFirstSampleNumber;
}
if (buffer.bytesLeft() < FlacConstants.MAX_FRAME_HEADER_SIZE) {
// The next frame header may not fit in the rest of the buffer, so put the trailing bytes at
// the start of the buffer, and reset the position and limit.
int bytesLeft = buffer.bytesLeft();
System.arraycopy(
buffer.getData(), buffer.getPosition(), buffer.getData(), /* destPos= */ 0, bytesLeft);
buffer.setPosition(0);
buffer.setLimit(bytesLeft);
}
return Extractor.RESULT_CONTINUE;
}
private SeekMap getSeekMap(long firstFramePosition, long streamLength) {
Assertions.checkNotNull(flacStreamMetadata);
if (flacStreamMetadata.seekTable != null) {
return new FlacSeekTableSeekMap(flacStreamMetadata, firstFramePosition);
} else if (streamLength != C.LENGTH_UNSET && flacStreamMetadata.totalSamples > 0) {
binarySearchSeeker =
new FlacBinarySearchSeeker(
flacStreamMetadata, frameStartMarker, firstFramePosition, streamLength);
return binarySearchSeeker.getSeekMap();
} else {
return new SeekMap.Unseekable(flacStreamMetadata.getDurationUs());
}
}
/**
* Searches for the start of a frame in {@code data}.
*
* <ul>
* <li>If the search is successful, the position is set to the start of the found frame.
* <li>Otherwise, the position is set to the first unsearched byte.
* </ul>
*
* @param data The array to be searched.
* @param foundEndOfInput If the end of input was met when filling in the {@code data}.
* @return The number of the first sample in the frame found, or {@code SAMPLE_NUMBER_UNKNOWN} if
* the search was not successful.
*/
private long findFrame(ParsableByteArray data, boolean foundEndOfInput) {
Assertions.checkNotNull(flacStreamMetadata);
int frameOffset = data.getPosition();
while (frameOffset <= data.limit() - FlacConstants.MAX_FRAME_HEADER_SIZE) {
data.setPosition(frameOffset);
if (FlacFrameReader.checkAndReadFrameHeader(
data, flacStreamMetadata, frameStartMarker, sampleNumberHolder)) {
data.setPosition(frameOffset);
return sampleNumberHolder.sampleNumber;
}
frameOffset++;
}
if (foundEndOfInput) {
// Verify whether there is a frame of size < MAX_FRAME_HEADER_SIZE at the end of the stream by
// checking at every position at a distance between MAX_FRAME_HEADER_SIZE and minFrameSize
// from the buffer limit if it corresponds to a valid frame header.
// At every offset, the different possibilities are:
// 1. The current offset indicates the start of a valid frame header. In this case, consider
// that a frame has been found and stop searching.
// 2. A frame starting at the current offset would be invalid. In this case, keep looking for
// a valid frame header.
// 3. The current offset could be the start of a valid frame header, but there is not enough
// bytes remaining to complete the header. As the end of the file has been reached, this
// means that the current offset does not correspond to a new frame and that the last bytes
// of the last frame happen to be a valid partial frame header. This case can occur in two
// ways:
// 3.1. An attempt to read past the buffer is made when reading the potential frame header.
// 3.2. Reading the potential frame header does not exceed the buffer size, but exceeds the
// buffer limit.
// Note that the third case is very unlikely. It never happens if the end of the input has not
// been reached as it is always made sure that the buffer has at least MAX_FRAME_HEADER_SIZE
// bytes available when reading a potential frame header.
while (frameOffset <= data.limit() - minFrameSize) {
data.setPosition(frameOffset);
boolean frameFound;
try {
frameFound =
FlacFrameReader.checkAndReadFrameHeader(
data, flacStreamMetadata, frameStartMarker, sampleNumberHolder);
} catch (IndexOutOfBoundsException e) {
// Case 3.1.
frameFound = false;
}
if (data.getPosition() > data.limit()) {
// TODO: Remove (and update above comments) once [Internal ref: b/147657250] is fixed.
// Case 3.2.
frameFound = false;
}
if (frameFound) {
// Case 1.
data.setPosition(frameOffset);
return sampleNumberHolder.sampleNumber;
}
frameOffset++;
}
// The end of the frame is the end of the file.
data.setPosition(data.limit());
} else {
data.setPosition(frameOffset);
}
return SAMPLE_NUMBER_UNKNOWN;
}
private void outputSampleMetadata() {
long timeUs =
currentFrameFirstSampleNumber
* C.MICROS_PER_SECOND
/ castNonNull(flacStreamMetadata).sampleRate;
castNonNull(trackOutput)
.sampleMetadata(
timeUs,
C.BUFFER_FLAG_KEY_FRAME,
currentFrameBytesWritten,
/* offset= */ 0,
/* encryptionData= */ null);
}
}