diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 49127a1099..a1b79518a9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,13 @@ # Release notes # +### 2.11.2 (TBD) ### + +* Add Java FLAC extractor + ([#6406](https://github.com/google/ExoPlayer/issues/6406)). + This extractor does not support seeking and live streams, and does not expose + vorbis, ID3 and picture data. If `DefaultExtractorsFactory` is used, this + extractor is only used if the FLAC extension is not loaded. + ### 2.11.1 (2019-12-20) ### * UI: Exclude `DefaultTimeBar` region from system gesture detection diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 01980c2f36..8550377ddf 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -411,6 +411,10 @@ "name": "Google Play (Ogg/Vorbis)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg" }, + { + "name": "Google Play (FLAC)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/flac/play.flac" + }, { "name": "Big Buck Bunny video (FLV)", "uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java index 4bfcc003ec..08f179152e 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java @@ -41,7 +41,7 @@ import java.nio.ByteBuffer; super( new FlacSeekTimestampConverter(streamMetadata), new FlacTimestampSeeker(decoderJni), - streamMetadata.durationUs(), + streamMetadata.getDurationUs(), /* floorTimePosition= */ 0, /* ceilingTimePosition= */ streamMetadata.totalSamples, /* floorBytePosition= */ firstFramePosition, diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index 890d82a006..e1f6112319 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -72,7 +72,7 @@ import java.util.List; int initialInputBufferSize = maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize; setInitialInputBufferSize(initialInputBufferSize); - maxOutputBufferSize = streamMetadata.maxDecodedFrameSize(); + maxOutputBufferSize = streamMetadata.getMaxDecodedFrameSize(); } @Override diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index fb5d41c0de..02a57dbf81 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacMetadataReader; import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -42,7 +43,6 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; -import java.util.Arrays; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -72,9 +72,6 @@ public final class FlacExtractor implements Extractor { */ public static final int FLAG_DISABLE_ID3_METADATA = 1; - /** FLAC stream marker */ - private static final byte[] FLAC_STREAM_MARKER = {'f', 'L', 'a', 'C'}; - private final ParsableByteArray outputBuffer; private final Id3Peeker id3Peeker; private final boolean id3MetadataDisabled; @@ -120,10 +117,8 @@ public final class FlacExtractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { - if (input.getPosition() == 0) { - id3Metadata = peekId3Data(input); - } - return peekFlacStreamMarker(input); + id3Metadata = peekId3Data(input); + return FlacMetadataReader.checkAndPeekStreamMarker(input); } @Override @@ -230,7 +225,7 @@ public final class FlacExtractor implements Extractor { metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata); } outputFormat(streamMetadata, metadata, trackOutput); - outputBuffer.reset(streamMetadata.maxDecodedFrameSize()); + outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } } @@ -251,18 +246,6 @@ public final class FlacExtractor implements Extractor { return seekResult; } - /** - * Peeks from the beginning of the input to see if {@link #FLAC_STREAM_MARKER} is present. - * - * @return Whether the input begins with {@link #FLAC_STREAM_MARKER}. - */ - private static boolean peekFlacStreamMarker(ExtractorInput input) - throws IOException, InterruptedException { - byte[] header = new byte[FLAC_STREAM_MARKER.length]; - input.peekFully(header, /* offset= */ 0, FLAC_STREAM_MARKER.length); - return Arrays.equals(header, FLAC_STREAM_MARKER); - } - /** * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to * handle seeks. @@ -277,14 +260,14 @@ public final class FlacExtractor implements Extractor { FlacBinarySearchSeeker binarySearchSeeker = null; SeekMap seekMap; if (haveSeekTable) { - seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni); + seekMap = new FlacSeekMap(streamMetadata.getDurationUs(), decoderJni); } else if (streamLength != C.LENGTH_UNSET) { long firstFramePosition = decoderJni.getDecodePosition(); binarySearchSeeker = new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni); seekMap = binarySearchSeeker.getSeekMap(); } else { - seekMap = new SeekMap.Unseekable(streamMetadata.durationUs()); + seekMap = new SeekMap.Unseekable(streamMetadata.getDurationUs()); } output.seekMap(seekMap); return binarySearchSeeker; @@ -297,8 +280,8 @@ public final class FlacExtractor implements Extractor { /* id= */ null, MimeTypes.AUDIO_RAW, /* codecs= */ null, - streamMetadata.bitRate(), - streamMetadata.maxDecodedFrameSize(), + streamMetadata.getBitRate(), + streamMetadata.getMaxDecodedFrameSize(), streamMetadata.channels, streamMetadata.sampleRate, getPcmEncoding(streamMetadata.bitsPerSample), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 02c676dfdf..26f250feea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.extractor.amr.AmrExtractor; +import com.google.android.exoplayer2.extractor.flac.FlacExtractor; import com.google.android.exoplayer2.extractor.flv.FlvExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; @@ -55,12 +56,13 @@ import java.lang.reflect.Constructor; */ public final class DefaultExtractorsFactory implements ExtractorsFactory { - private static final Constructor extends Extractor> FLAC_EXTRACTOR_CONSTRUCTOR; + private static final Constructor extends Extractor> FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR; + static { - Constructor extends Extractor> flacExtractorConstructor = null; + Constructor extends Extractor> flacExtensionExtractorConstructor = null; try { // LINT.IfChange - flacExtractorConstructor = + flacExtensionExtractorConstructor = Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") .asSubclass(Extractor.class) .getConstructor(); @@ -71,7 +73,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { // The FLAC extension is present, but instantiation failed. throw new RuntimeException("Error instantiating FLAC extension", e); } - FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor; + FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR = flacExtensionExtractorConstructor; } private boolean constantBitrateSeekingEnabled; @@ -208,7 +210,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @Override public synchronized Extractor[] createExtractors() { - Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 13 : 14]; + Extractor[] extractors = new Extractor[14]; extractors[0] = new MatroskaExtractor(matroskaFlags); extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags); extractors[2] = new Mp4Extractor(mp4Flags); @@ -237,13 +239,16 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING : 0)); extractors[12] = new Ac4Extractor(); - if (FLAC_EXTRACTOR_CONSTRUCTOR != null) { + // Prefer the FLAC extension extractor because it supports seeking. + if (FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR != null) { try { - extractors[13] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance(); + extractors[13] = FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance(); } catch (Exception e) { // Should never happen. throw new IllegalStateException("Unexpected error creating FLAC extractor", e); } + } else { + extractors[13] = new FlacExtractor(); } return extractors; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java new file mode 100644 index 0000000000..33f608788b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java @@ -0,0 +1,302 @@ +/* + * 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 androidx.annotation.IntDef; +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.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.FlacFrameReader; +import com.google.android.exoplayer2.util.FlacFrameReader.BlockSizeHolder; +import com.google.android.exoplayer2.util.FlacMetadataReader; +import com.google.android.exoplayer2.util.FlacMetadataReader.FirstFrameMetadata; +import com.google.android.exoplayer2.util.FlacStreamMetadata; +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; + +// TODO: implement seeking. +// TODO: expose vorbis and ID3 data. +// TODO: support live streams. +/** + * Extracts data from FLAC container format. + * + *
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()}; + + /** Parser state. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_READ_ID3_TAG, + STATE_READ_STREAM_MARKER, + STATE_READ_STREAM_INFO_BLOCK, + STATE_SKIP_OPTIONAL_METADATA_BLOCKS, + STATE_GET_FIRST_FRAME_METADATA, + STATE_READ_FRAMES + }) + private @interface State {} + + private static final int STATE_READ_ID3_TAG = 0; + private static final int STATE_READ_STREAM_MARKER = 1; + private static final int STATE_READ_STREAM_INFO_BLOCK = 2; + private static final int STATE_SKIP_OPTIONAL_METADATA_BLOCKS = 3; + private static final int STATE_GET_FIRST_FRAME_METADATA = 4; + private static final int STATE_READ_FRAMES = 5; + + /** Arbitrary scratch length of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */ + private static final int SCRATCH_LENGTH = 32 * 1024; + + /** Value of an unknown block size. */ + private static final int BLOCK_SIZE_UNKNOWN = -1; + + private final byte[] streamMarkerAndInfoBlock; + private final ParsableByteArray scratch; + + private final BlockSizeHolder blockSizeHolder; + + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private TrackOutput trackOutput; + + private @State int state; + @MonotonicNonNull private FlacStreamMetadata flacStreamMetadata; + private int minFrameSize; + private int frameStartMarker; + private int currentFrameBlockSizeSamples; + private int currentFrameBytesWritten; + private long totalSamplesWritten; + + public FlacExtractor() { + streamMarkerAndInfoBlock = + new byte[FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE]; + scratch = new ParsableByteArray(SCRATCH_LENGTH); + blockSizeHolder = new BlockSizeHolder(); + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + FlacMetadataReader.peekId3Data(input); + 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 int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + switch (state) { + case STATE_READ_ID3_TAG: + readId3Tag(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_STREAM_MARKER: + readStreamMarker(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_STREAM_INFO_BLOCK: + readStreamInfoBlock(input); + return Extractor.RESULT_CONTINUE; + case STATE_SKIP_OPTIONAL_METADATA_BLOCKS: + skipOptionalMetadataBlocks(input); + return Extractor.RESULT_CONTINUE; + case STATE_GET_FIRST_FRAME_METADATA: + getFirstFrameMetadata(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_FRAMES: + return readFrames(input); + default: + throw new IllegalStateException(); + } + } + + @Override + public void seek(long position, long timeUs) { + state = STATE_READ_ID3_TAG; + currentFrameBytesWritten = 0; + totalSamplesWritten = 0; + scratch.reset(); + } + + @Override + public void release() { + // Do nothing. + } + + // Private methods. + + private void readId3Tag(ExtractorInput input) throws IOException, InterruptedException { + FlacMetadataReader.readId3Data(input); + state = STATE_READ_STREAM_MARKER; + } + + private void readStreamMarker(ExtractorInput input) throws IOException, InterruptedException { + FlacMetadataReader.readStreamMarker( + input, streamMarkerAndInfoBlock, /* scratchWriteIndex= */ 0); + state = STATE_READ_STREAM_INFO_BLOCK; + } + + private void readStreamInfoBlock(ExtractorInput input) throws IOException, InterruptedException { + flacStreamMetadata = + FlacMetadataReader.readStreamInfoBlock( + input, + /* scratchData= */ streamMarkerAndInfoBlock, + /* scratchWriteIndex= */ FlacConstants.STREAM_MARKER_SIZE); + minFrameSize = Math.max(flacStreamMetadata.minFrameSize, FlacConstants.MIN_FRAME_HEADER_SIZE); + boolean isLastMetadataBlock = + (streamMarkerAndInfoBlock[FlacConstants.STREAM_MARKER_SIZE] >> 7 & 1) == 1; + castNonNull(trackOutput).format(flacStreamMetadata.getFormat(streamMarkerAndInfoBlock)); + castNonNull(extractorOutput) + .seekMap(new SeekMap.Unseekable(flacStreamMetadata.getDurationUs())); + + if (isLastMetadataBlock) { + state = STATE_GET_FIRST_FRAME_METADATA; + } else { + state = STATE_SKIP_OPTIONAL_METADATA_BLOCKS; + } + } + + private void skipOptionalMetadataBlocks(ExtractorInput input) + throws IOException, InterruptedException { + FlacMetadataReader.skipMetadataBlocks(input); + state = STATE_GET_FIRST_FRAME_METADATA; + } + + private void getFirstFrameMetadata(ExtractorInput input) + throws IOException, InterruptedException { + FirstFrameMetadata firstFrameMetadata = FlacMetadataReader.getFirstFrameMetadata(input); + frameStartMarker = firstFrameMetadata.frameStartMarker; + currentFrameBlockSizeSamples = firstFrameMetadata.blockSizeSamples; + + state = STATE_READ_FRAMES; + } + + // TODO: consider sending bytes within min frame size directly from the input to the sample queue + // to avoid unnecessary copies in scratch. + private int readFrames(ExtractorInput input) throws IOException, InterruptedException { + Assertions.checkNotNull(trackOutput); + Assertions.checkNotNull(flacStreamMetadata); + + // Copy more bytes into the scratch. + int currentLimit = scratch.limit(); + int bytesRead = + input.read( + scratch.data, /* offset= */ currentLimit, /* length= */ SCRATCH_LENGTH - currentLimit); + boolean foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; + if (!foundEndOfInput) { + scratch.setLimit(currentLimit + bytesRead); + } else if (scratch.bytesLeft() == 0) { + return C.RESULT_END_OF_INPUT; + } + + // Search for a frame. + int positionBeforeFindingAFrame = scratch.getPosition(); + + // Skip frame search on the bytes within the minimum frame size. + if (currentFrameBytesWritten < minFrameSize) { + scratch.skipBytes(Math.min(minFrameSize, scratch.bytesLeft())); + } + + int nextFrameBlockSizeSamples = findFrame(scratch, foundEndOfInput); + int numberOfFrameBytes = scratch.getPosition() - positionBeforeFindingAFrame; + scratch.setPosition(positionBeforeFindingAFrame); + trackOutput.sampleData(scratch, numberOfFrameBytes); + currentFrameBytesWritten += numberOfFrameBytes; + + // Frame found. + if (nextFrameBlockSizeSamples != BLOCK_SIZE_UNKNOWN || foundEndOfInput) { + long timeUs = getTimeUs(totalSamplesWritten, flacStreamMetadata.sampleRate); + trackOutput.sampleMetadata( + timeUs, + C.BUFFER_FLAG_KEY_FRAME, + currentFrameBytesWritten, + /* offset= */ 0, + /* encryptionData= */ null); + totalSamplesWritten += currentFrameBlockSizeSamples; + currentFrameBytesWritten = 0; + currentFrameBlockSizeSamples = nextFrameBlockSizeSamples; + } + + if (scratch.bytesLeft() < FlacConstants.MAX_FRAME_HEADER_SIZE) { + // The next frame header may not fit in the rest of the scratch, so put the trailing bytes at + // the start of the scratch, and reset the position and limit. + System.arraycopy( + scratch.data, scratch.getPosition(), scratch.data, /* destPos= */ 0, scratch.bytesLeft()); + scratch.reset(scratch.bytesLeft()); + } + + return Extractor.RESULT_CONTINUE; + } + + /** + * Searches for the start of a frame in {@code scratch}. + * + *
If the header is valid, the position of {@code scratch} is moved to the byte following it. + * Otherwise, there is no guarantee on the position. + * + * @param scratch The array to read the data from, whose position must correspond to the frame + * header. + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker of the stream. + * @param blockSizeHolder The holder used to contain the block size. + * @return Whether the frame header is valid. + */ + public static boolean checkAndReadFrameHeader( + ParsableByteArray scratch, + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + BlockSizeHolder blockSizeHolder) { + int frameStartPosition = scratch.getPosition(); + + long frameHeaderBytes = scratch.readUnsignedInt(); + if (frameHeaderBytes >>> 16 != frameStartMarker) { + return false; + } + + int blockSizeKey = (int) (frameHeaderBytes >> 12 & 0xF); + int sampleRateKey = (int) (frameHeaderBytes >> 8 & 0xF); + int channelAssignmentKey = (int) (frameHeaderBytes >> 4 & 0xF); + int bitsPerSampleKey = (int) (frameHeaderBytes >> 1 & 0x7); + boolean reservedBit = (frameHeaderBytes & 1) == 1; + return checkChannelAssignment(channelAssignmentKey, flacStreamMetadata) + && checkBitsPerSample(bitsPerSampleKey, flacStreamMetadata) + && !reservedBit + && checkAndReadUtf8Data(scratch) + && checkAndReadBlockSizeSamples(scratch, flacStreamMetadata, blockSizeKey, blockSizeHolder) + && checkAndReadSampleRate(scratch, flacStreamMetadata, sampleRateKey) + && checkAndReadCrc(scratch, frameStartPosition); + } + + /** + * Returns the block size of the given frame. + * + *
If no exception is thrown, the position of {@code scratch} is left unchanged. Otherwise, + * there is no guarantee on the position. + * + * @param scratch The array to get the data from, whose position must correspond to the start of a + * frame. + * @return The block size in samples, or -1 if the block size is invalid. + */ + public static int getFrameBlockSizeSamples(ParsableByteArray scratch) { + int blockSizeKey = (scratch.data[2] & 0xFF) >> 4; + if (blockSizeKey < 6 || blockSizeKey > 7) { + return readFrameBlockSizeSamplesFromKey(scratch, blockSizeKey); + } + scratch.skipBytes(4); + scratch.readUtf8EncodedLong(); + int blockSizeSamples = readFrameBlockSizeSamplesFromKey(scratch, blockSizeKey); + scratch.setPosition(0); + return blockSizeSamples; + } + + /** + * Reads the given block size. + * + * @param scratch The array to read the data from, whose position must correspond to the block + * size bits. + * @param blockSizeKey The key in the block size lookup table. + * @return The block size in samples. + */ + public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray scratch, int blockSizeKey) { + switch (blockSizeKey) { + case 1: + return 192; + case 2: + case 3: + case 4: + case 5: + return 576 << (blockSizeKey - 2); + case 6: + return scratch.readUnsignedByte() + 1; + case 7: + return scratch.readUnsignedShort() + 1; + case 8: + case 9: + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + return 256 << (blockSizeKey - 8); + default: + return -1; + } + } + + /** + * Checks whether the given channel assignment is valid. + * + * @param channelAssignmentKey The channel assignment lookup key. + * @param flacStreamMetadata The stream metadata. + * @return Whether the channel assignment is valid. + */ + private static boolean checkChannelAssignment( + int channelAssignmentKey, FlacStreamMetadata flacStreamMetadata) { + if (channelAssignmentKey <= 7) { + return channelAssignmentKey == flacStreamMetadata.channels - 1; + } else if (channelAssignmentKey <= 10) { + return flacStreamMetadata.channels == 2; + } else { + return false; + } + } + + /** + * Checks whether the given number of bits per sample is valid. + * + * @param bitsPerSampleKey The bits per sample lookup key. + * @param flacStreamMetadata The stream metadata. + * @return Whether the number of bits per sample is valid. + */ + private static boolean checkBitsPerSample( + int bitsPerSampleKey, FlacStreamMetadata flacStreamMetadata) { + if (bitsPerSampleKey == 0) { + return true; + } + return bitsPerSampleKey == flacStreamMetadata.bitsPerSampleLookupKey; + } + + /** + * Checks whether the given UTF-8 data is valid and, if so, reads it. + * + *
If the UTF-8 data is valid, the position of {@code scratch} is moved to the byte following + * it. Otherwise, there is no guarantee on the position. + * + * @param scratch The array to read the data from, whose position must correspond to the UTF-8 + * data. + * @return Whether the UTF-8 data is valid. + */ + private static boolean checkAndReadUtf8Data(ParsableByteArray scratch) { + try { + scratch.readUtf8EncodedLong(); + } catch (NumberFormatException e) { + return false; + } + return true; + } + + /** + * Checks whether the given frame block size key and block size bits are valid and, if so, reads + * the block size bits and writes the block size in {@code blockSizeHolder}. + * + *
If the block size is valid, the position of {@code scratch} is moved to the byte following + * the block size bits. Otherwise, there is no guarantee on the position. + * + * @param scratch The array to read the data from, whose position must correspond to the block + * size bits. + * @param flacStreamMetadata The stream metadata. + * @param blockSizeKey The key in the block size lookup table. + * @param blockSizeHolder The holder used to contain the block size. + * @return Whether the block size is valid. + */ + private static boolean checkAndReadBlockSizeSamples( + ParsableByteArray scratch, + FlacStreamMetadata flacStreamMetadata, + int blockSizeKey, + BlockSizeHolder blockSizeHolder) { + int blockSizeSamples = readFrameBlockSizeSamplesFromKey(scratch, blockSizeKey); + if (blockSizeSamples == -1 || blockSizeSamples > flacStreamMetadata.maxBlockSizeSamples) { + return false; + } + blockSizeHolder.blockSizeSamples = blockSizeSamples; + return true; + } + + /** + * Checks whether the given sample rate key and sample rate bits are valid and, if so, reads the + * sample rate bits. + * + *
If the sample rate is valid, the position of {@code scratch} is moved to the byte following + * the sample rate bits. Otherwise, there is no guarantee on the position. + * + * @param scratch The array to read the data from, whose position must indicate the sample rate + * bits. + * @param flacStreamMetadata The stream metadata. + * @param sampleRateKey The key in the sample rate lookup table. + * @return Whether the sample rate is valid. + */ + private static boolean checkAndReadSampleRate( + ParsableByteArray scratch, FlacStreamMetadata flacStreamMetadata, int sampleRateKey) { + int expectedSampleRate = flacStreamMetadata.sampleRate; + if (sampleRateKey == 0) { + return true; + } else if (sampleRateKey <= 11) { + return sampleRateKey == flacStreamMetadata.sampleRateLookupKey; + } else if (sampleRateKey == 12) { + return scratch.readUnsignedByte() * 1000 == expectedSampleRate; + } else if (sampleRateKey <= 14) { + int sampleRate = scratch.readUnsignedShort(); + if (sampleRateKey == 14) { + sampleRate *= 10; + } + return sampleRate == expectedSampleRate; + } else { + return false; + } + } + + /** + * Checks whether the given CRC is valid and, if so, reads it. + * + *
If the CRC is valid, the position of {@code scratch} is moved to the byte following it. + * Otherwise, there is no guarantee on the position. + * + *
The {@code scratch} array must contain the whole frame header. + * + * @param scratch The array to read the data from, whose position must indicate the CRC. + * @param frameStartPosition The frame start offset in {@code scratch}. + * @return Whether the CRC is valid. + */ + private static boolean checkAndReadCrc(ParsableByteArray scratch, int frameStartPosition) { + int crc = scratch.readUnsignedByte(); + int frameEndPosition = scratch.getPosition(); + int expectedCrc = + Util.crc8(scratch.data, frameStartPosition, frameEndPosition - 1, /* initialValue= */ 0); + return crc == expectedCrc; + } + + private FlacFrameReader() {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacMetadataReader.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacMetadataReader.java new file mode 100644 index 0000000000..23eefd042c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacMetadataReader.java @@ -0,0 +1,208 @@ +/* + * 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.util; + +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.Id3Peeker; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import java.io.IOException; + +/** Reads and peeks FLAC stream metadata elements from an {@link ExtractorInput}. */ +public final class FlacMetadataReader { + + /** Holds the metadata extracted from the first frame. */ + public static final class FirstFrameMetadata { + /** The frame start marker, which should correspond to the 2 first bytes of each frame. */ + public final int frameStartMarker; + /** The block size in samples. */ + public final int blockSizeSamples; + + public FirstFrameMetadata(int frameStartMarker, int blockSizeSamples) { + this.frameStartMarker = frameStartMarker; + this.blockSizeSamples = blockSizeSamples; + } + } + + private static final int STREAM_MARKER = 0x664C6143; // ASCII for "fLaC" + private static final int SYNC_CODE = 0x3FFE; + + /** + * Peeks ID3 Data. + * + * @param input Input stream to peek the ID3 data from. + * @throws IOException If peeking from the input fails. In this case, there is no guarantee on the + * peek position. + * @throws InterruptedException If interrupted while peeking from input. In this case, there is no + * guarantee on the peek position. + */ + public static void peekId3Data(ExtractorInput input) throws IOException, InterruptedException { + new Id3Peeker().peekId3Data(input, Id3Decoder.NO_FRAMES_PREDICATE); + } + + /** + * Peeks the FLAC stream marker. + * + * @param input Input stream to peek the stream marker from. + * @return Whether the data peeked is the FLAC stream marker. + * @throws IOException If peeking from the input fails. In this case, the peek position is left + * unchanged. + * @throws InterruptedException If interrupted while peeking from input. In this case, the peek + * position is left unchanged. + */ + public static boolean checkAndPeekStreamMarker(ExtractorInput input) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE); + input.peekFully(scratch.data, /* offset= */ 0, FlacConstants.STREAM_MARKER_SIZE); + return scratch.readUnsignedInt() == STREAM_MARKER; + } + + /** + * Reads ID3 Data. + * + *
If no exception is thrown, the peek position of {@code input} is aligned with the read + * position. + * + * @param input Input stream to read the ID3 data from. + * @throws IOException If reading from the input fails. In this case, the read position is left + * unchanged and there is no guarantee on the peek position. + * @throws InterruptedException If interrupted while reading from input. In this case, the read + * position is left unchanged and there is no guarantee on the peek position. + */ + public static void readId3Data(ExtractorInput input) throws IOException, InterruptedException { + input.resetPeekPosition(); + long startingPeekPosition = input.getPeekPosition(); + new Id3Peeker().peekId3Data(input, Id3Decoder.NO_FRAMES_PREDICATE); + int peekedId3Bytes = (int) (input.getPeekPosition() - startingPeekPosition); + input.skipFully(peekedId3Bytes); + } + + /** + * Reads the FLAC stream marker. + * + * @param input Input stream to read the stream marker from. + * @param scratchData The array in which the data read should be copied. This array must have size + * at least {@code scratchWriteIndex} + {@link FlacConstants#STREAM_MARKER_SIZE}. + * @param scratchWriteIndex The index of {@code scratchData} from which to write. + * @throws ParserException If an error occurs parsing the stream marker. In this case, the + * position of {@code input} is advanced by {@link FlacConstants#STREAM_MARKER_SIZE} bytes. + * @throws IOException If reading from the input fails. In this case, the position is left + * unchanged. + * @throws InterruptedException If interrupted while reading from input. In this case, the + * position is left unchanged. + */ + public static void readStreamMarker( + ExtractorInput input, byte[] scratchData, int scratchWriteIndex) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(scratchData); + input.readFully( + scratch.data, + /* offset= */ scratchWriteIndex, + /* length= */ FlacConstants.STREAM_MARKER_SIZE); + scratch.setPosition(scratchWriteIndex); + if (scratch.readUnsignedInt() != STREAM_MARKER) { + throw new ParserException("Failed to read FLAC stream marker."); + } + } + + /** + * Reads the stream info block. + * + * @param input Input stream to read the stream info block from. + * @param scratchData The array in which the data read should be copied. This array must have size + * at least {@code scratchWriteIndex} + {@link FlacConstants#STREAM_INFO_BLOCK_SIZE}. + * @param scratchWriteIndex The index of {@code scratchData} from which to write. + * @return A new {@link FlacStreamMetadata} read from {@code input}. + * @throws IOException If reading from the input fails. In this case, the position is left + * unchanged. + * @throws InterruptedException If interrupted while reading from input. In this case, the + * position is left unchanged. + */ + public static FlacStreamMetadata readStreamInfoBlock( + ExtractorInput input, byte[] scratchData, int scratchWriteIndex) + throws IOException, InterruptedException { + input.readFully( + scratchData, + /* offset= */ scratchWriteIndex, + /* length= */ FlacConstants.STREAM_INFO_BLOCK_SIZE); + return new FlacStreamMetadata( + scratchData, /* offset= */ scratchWriteIndex + FlacConstants.METADATA_BLOCK_HEADER_SIZE); + } + + /** + * Skips the stream metadata blocks. + * + *
If no exception is thrown, the peek position of {@code input} is aligned with the read + * position. + * + * @param input Input stream to read the metadata blocks from. + * @throws IOException If reading from the input fails. In this case, the read position will be at + * the start of a metadata block and there is no guarantee on the peek position. + * @throws InterruptedException If interrupted while reading from input. In this case, the read + * position will be at the start of a metadata block and there is no guarantee on the peek + * position. + */ + public static void skipMetadataBlocks(ExtractorInput input) + throws IOException, InterruptedException { + input.resetPeekPosition(); + ParsableBitArray scratch = new ParsableBitArray(new byte[4]); + boolean lastMetadataBlock = false; + while (!lastMetadataBlock) { + input.peekFully(scratch.data, /* offset= */ 0, /* length= */ 4); + scratch.setPosition(0); + lastMetadataBlock = scratch.readBit(); + scratch.skipBits(7); + int length = scratch.readBits(24); + input.skipFully(4 + length); + } + } + + /** + * Returns some metadata extracted from the first frame of a FLAC stream. + * + *
The read position of {@code input} is left unchanged and the peek position is aligned with + * the read position. + * + * @param input Input stream to get the metadata from (starting from the read position). + * @return A {@link FirstFrameMetadata} containing the frame start marker (which should be the + * same for all the frames in the stream) and the block size of the frame. + * @throws ParserException If an error occurs parsing the frame metadata. + * @throws IOException If peeking from the input fails. + * @throws InterruptedException If interrupted while peeking from input. + */ + public static FirstFrameMetadata getFirstFrameMetadata(ExtractorInput input) + throws IOException, InterruptedException { + input.resetPeekPosition(); + ParsableByteArray scratch = + new ParsableByteArray(new byte[FlacConstants.MAX_FRAME_HEADER_SIZE]); + input.peekFully(scratch.data, /* offset= */ 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + int frameStartMarker = scratch.readUnsignedShort(); + int syncCode = frameStartMarker >> 2; + if (syncCode != SYNC_CODE) { + input.resetPeekPosition(); + throw new ParserException("First frame does not start with sync code."); + } + + scratch.setPosition(0); + int firstFrameBlockSizeSamples = FlacFrameReader.getFrameBlockSizeSamples(scratch); + + input.resetPeekPosition(); + return new FirstFrameMetadata(frameStartMarker, firstFrameBlockSizeSamples); + } + + private FlacMetadataReader() {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java index 9c5862b483..b35d585a05 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -17,10 +17,12 @@ package com.google.android.exoplayer2.util; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.flac.PictureFrame; import com.google.android.exoplayer2.metadata.flac.VorbisComment; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** Holder for FLAC metadata. */ @@ -28,14 +30,45 @@ public final class FlacStreamMetadata { private static final String TAG = "FlacStreamMetadata"; - public final int minBlockSize; - public final int maxBlockSize; + /** Indicates that a value is not in the corresponding lookup table. */ + public static final int NOT_IN_LOOKUP_TABLE = -1; + + /** Minimum number of samples per block. */ + public final int minBlockSizeSamples; + /** Maximum number of samples per block. */ + public final int maxBlockSizeSamples; + /** Minimum frame size in bytes, or 0 if the value is unknown. */ public final int minFrameSize; + /** Maximum frame size in bytes, or 0 if the value is unknown. */ public final int maxFrameSize; + /** Sample rate in Hertz. */ public final int sampleRate; + /** + * Lookup key corresponding to the stream sample rate, or {@link #NOT_IN_LOOKUP_TABLE} if it is + * not in the lookup table. + * + *
This key is used to indicate the sample rate in the frame header for the most common values. + * + *
The sample rate lookup table is described in https://xiph.org/flac/format.html#frame_header. + */ + public final int sampleRateLookupKey; + /** Number of audio channels. */ public final int channels; + /** Number of bits per sample. */ public final int bitsPerSample; + /** + * Lookup key corresponding to the number of bits per sample of the stream, or {@link + * #NOT_IN_LOOKUP_TABLE} if it is not in the lookup table. + * + *
This key is used to indicate the number of bits per sample in the frame header for the most + * common values. + * + *
The sample size lookup table is described in https://xiph.org/flac/format.html#frame_header.
+ */
+ public final int bitsPerSampleLookupKey;
+ /** Total number of samples, or 0 if the value is unknown. */
public final long totalSamples;
+ /** Stream content metadata. */
@Nullable public final Metadata metadata;
private static final String SEPARATOR = "=";
@@ -44,27 +77,29 @@ public final class FlacStreamMetadata {
* Parses binary FLAC stream info metadata.
*
* @param data An array containing binary FLAC stream info metadata.
- * @param offset The offset of the stream info metadata in {@code data}.
+ * @param offset The offset of the stream info block in {@code data} (header excluded).
* @see FLAC format
* METADATA_BLOCK_STREAMINFO
*/
public FlacStreamMetadata(byte[] data, int offset) {
ParsableBitArray scratch = new ParsableBitArray(data);
scratch.setPosition(offset * 8);
- this.minBlockSize = scratch.readBits(16);
- this.maxBlockSize = scratch.readBits(16);
+ this.minBlockSizeSamples = scratch.readBits(16);
+ this.maxBlockSizeSamples = scratch.readBits(16);
this.minFrameSize = scratch.readBits(24);
this.maxFrameSize = scratch.readBits(24);
this.sampleRate = scratch.readBits(20);
+ this.sampleRateLookupKey = getSampleRateLookupKey();
this.channels = scratch.readBits(3) + 1;
this.bitsPerSample = scratch.readBits(5) + 1;
- this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL);
+ this.bitsPerSampleLookupKey = getBitsPerSampleLookupKey();
+ this.totalSamples = scratch.readBitsToLong(36);
this.metadata = null;
}
/**
- * @param minBlockSize Minimum block size of the FLAC stream.
- * @param maxBlockSize Maximum block size of the FLAC stream.
+ * @param minBlockSizeSamples Minimum block size of the FLAC stream.
+ * @param maxBlockSizeSamples Maximum block size of the FLAC stream.
* @param minFrameSize Minimum frame size of the FLAC stream.
* @param maxFrameSize Maximum frame size of the FLAC stream.
* @param sampleRate Sample rate of the FLAC stream.
@@ -81,8 +116,8 @@ public final class FlacStreamMetadata {
* METADATA_BLOCK_PICTURE
*/
public FlacStreamMetadata(
- int minBlockSize,
- int maxBlockSize,
+ int minBlockSizeSamples,
+ int maxBlockSizeSamples,
int minFrameSize,
int maxFrameSize,
int sampleRate,
@@ -91,30 +126,35 @@ public final class FlacStreamMetadata {
long totalSamples,
List {@code streamMarkerAndInfoBlock} is updated to set the bit corresponding to the stream info
+ * last metadata block flag to true.
+ *
+ * @param streamMarkerAndInfoBlock An array containing the FLAC stream marker followed by the
+ * stream info block.
+ * @return The extracted {@link Format}.
+ */
+ public Format getFormat(byte[] streamMarkerAndInfoBlock) {
+ // Set the last metadata block flag, ignore the other blocks.
+ streamMarkerAndInfoBlock[4] = (byte) 0x80;
+ int maxInputSize = maxFrameSize > 0 ? maxFrameSize : Format.NO_VALUE;
+ return Format.createAudioSampleFormat(
+ /* id= */ null,
+ MimeTypes.AUDIO_FLAC,
+ /* codecs= */ null,
+ getBitRate(),
+ maxInputSize,
+ channels,
+ sampleRate,
+ Collections.singletonList(streamMarkerAndInfoBlock),
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null);
+ }
+
+ private int getSampleRateLookupKey() {
+ switch (sampleRate) {
+ case 88200:
+ return 1;
+ case 176400:
+ return 2;
+ case 192000:
+ return 3;
+ case 8000:
+ return 4;
+ case 16000:
+ return 5;
+ case 22050:
+ return 6;
+ case 24000:
+ return 7;
+ case 32000:
+ return 8;
+ case 44100:
+ return 9;
+ case 48000:
+ return 10;
+ case 96000:
+ return 11;
+ default:
+ return NOT_IN_LOOKUP_TABLE;
+ }
+ }
+
+ private int getBitsPerSampleLookupKey() {
+ switch (bitsPerSample) {
+ case 8:
+ return 1;
+ case 12:
+ return 2;
+ case 16:
+ return 4;
+ case 20:
+ return 5;
+ case 24:
+ return 6;
+ default:
+ return NOT_IN_LOOKUP_TABLE;
+ }
+ }
+
@Nullable
- private static Metadata buildMetadata(
+ private static Metadata getMetadata(
List