summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java411
1 files changed, 411 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java
new file mode 100644
index 0000000000..fa997001e8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java
@@ -0,0 +1,411 @@
+/*
+ * 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.flac;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+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.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.FlacFrameReader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacMetadataReader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap;
+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.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata;
+import org.mozilla.thirdparty.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 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;
+
+ @MonotonicNonNull private ExtractorOutput extractorOutput;
+ @MonotonicNonNull private TrackOutput trackOutput;
+
+ private @State int state;
+ @Nullable private Metadata id3Metadata;
+ @MonotonicNonNull private FlacStreamMetadata flacStreamMetadata;
+ private int minFrameSize;
+ private int frameStartMarker;
+ @MonotonicNonNull private 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, InterruptedException {
+ 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, InterruptedException {
+ 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();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing.
+ }
+
+ // Private methods.
+
+ private void readId3Metadata(ExtractorInput input) throws IOException, InterruptedException {
+ id3Metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ !id3MetadataDisabled);
+ state = STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES;
+ }
+
+ private void getStreamMarkerAndInfoBlockBytes(ExtractorInput input)
+ throws IOException, InterruptedException {
+ input.peekFully(streamMarkerAndInfoBlock, 0, streamMarkerAndInfoBlock.length);
+ input.resetPeekPosition();
+ state = STATE_READ_STREAM_MARKER;
+ }
+
+ private void readStreamMarker(ExtractorInput input) throws IOException, InterruptedException {
+ FlacMetadataReader.readStreamMarker(input);
+ state = STATE_READ_METADATA_BLOCKS;
+ }
+
+ private void readMetadataBlocks(ExtractorInput input) throws IOException, InterruptedException {
+ 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 = Math.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, InterruptedException {
+ 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, InterruptedException {
+ 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.data, /* 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(Math.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.
+ System.arraycopy(
+ buffer.data, buffer.getPosition(), buffer.data, /* destPos= */ 0, buffer.bytesLeft());
+ buffer.reset(buffer.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);
+ }
+}