| /* |
| * 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); |
| } |
| } |