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 FLAC_EXTRACTOR_CONSTRUCTOR; + private static final Constructor FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR; + static { - Constructor flacExtractorConstructor = null; + Constructor 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}. + * + *

+ * + * @param scratch The array to be searched. + * @param foundEndOfInput If the end of input was met when filling in the {@code scratch}. + * @return The block size of the frame found, or {@code BLOCK_SIZE_UNKNOWN} if the search was not + * successful. + */ + private int findFrame(ParsableByteArray scratch, boolean foundEndOfInput) { + Assertions.checkNotNull(flacStreamMetadata); + + int frameOffset = scratch.getPosition(); + while (frameOffset <= scratch.limit() - FlacConstants.MAX_FRAME_HEADER_SIZE) { + scratch.setPosition(frameOffset); + if (FlacFrameReader.checkAndReadFrameHeader( + scratch, flacStreamMetadata, frameStartMarker, blockSizeHolder)) { + scratch.setPosition(frameOffset); + return blockSizeHolder.blockSizeSamples; + } + frameOffset++; + } + + if (foundEndOfInput) { + // Reached the end of the file. Assume it's the end of the frame. + scratch.setPosition(scratch.limit()); + } else { + scratch.setPosition(frameOffset); + } + + return BLOCK_SIZE_UNKNOWN; + } + + private long getTimeUs(long numSamples, int sampleRate) { + return numSamples * C.MICROS_PER_SECOND / sampleRate; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java index cef274b903..ed86944f1e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -15,18 +15,14 @@ */ package com.google.android.exoplayer2.extractor.ogg; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.FlacStreamMetadata; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.Arrays; -import java.util.Collections; -import java.util.List; /** * {@link StreamReader} to extract Flac data out of Ogg byte stream. @@ -72,24 +68,8 @@ import java.util.List; byte[] data = packet.data; if (streamMetadata == null) { streamMetadata = new FlacStreamMetadata(data, 17); - int maxInputSize = - streamMetadata.maxFrameSize == 0 ? Format.NO_VALUE : streamMetadata.maxFrameSize; byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); - metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks - List initializationData = Collections.singletonList(metadata); - setupData.format = - Format.createAudioSampleFormat( - /* id= */ null, - MimeTypes.AUDIO_FLAC, - /* codecs= */ null, - streamMetadata.bitRate(), - maxInputSize, - streamMetadata.channels, - streamMetadata.sampleRate, - initializationData, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null); + setupData.format = streamMetadata.getFormat(metadata); } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) { flacOggSeeker = new FlacOggSeeker(); flacOggSeeker.parseSeekTable(packet); @@ -220,7 +200,7 @@ import java.util.List; @Override public long getDurationUs() { - return streamMetadata.durationUs(); + return streamMetadata.getDurationUs(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacConstants.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacConstants.java new file mode 100644 index 0000000000..75b153d6f9 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacConstants.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** Defines constants used by the FLAC extractor. */ +public final class FlacConstants { + + /** Size of the FLAC stream marker in bytes. */ + public static final int STREAM_MARKER_SIZE = 4; + /** Size of the header of a FLAC metadata block in bytes. */ + public static final int METADATA_BLOCK_HEADER_SIZE = 4; + /** Size of the FLAC stream info block (header included) in bytes. */ + public static final int STREAM_INFO_BLOCK_SIZE = 38; + /** Minimum size of a FLAC frame header in bytes. */ + public static final int MIN_FRAME_HEADER_SIZE = 6; + /** Maximum size of a FLAC frame header in bytes. */ + public static final int MAX_FRAME_HEADER_SIZE = 16; + + private FlacConstants() {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacFrameReader.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacFrameReader.java new file mode 100644 index 0000000000..71317494e0 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacFrameReader.java @@ -0,0 +1,257 @@ +/* + * 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; + +/** Reads and peeks FLAC frame elements. */ +public final class FlacFrameReader { + + /** Holds a frame block size. */ + public static final class BlockSizeHolder { + /** The block size in samples. */ + public int blockSizeSamples; + } + + /** + * Checks whether the given FLAC frame header is valid and, if so, reads it and writes the frame + * block size in {@code blockSizeHolder}. + * + *

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 vorbisComments, List pictureFrames) { - this.minBlockSize = minBlockSize; - this.maxBlockSize = maxBlockSize; + this.minBlockSizeSamples = minBlockSizeSamples; + this.maxBlockSizeSamples = maxBlockSizeSamples; this.minFrameSize = minFrameSize; this.maxFrameSize = maxFrameSize; this.sampleRate = sampleRate; + this.sampleRateLookupKey = getSampleRateLookupKey(); this.channels = channels; this.bitsPerSample = bitsPerSample; + this.bitsPerSampleLookupKey = getBitsPerSampleLookupKey(); this.totalSamples = totalSamples; - this.metadata = buildMetadata(vorbisComments, pictureFrames); + this.metadata = getMetadata(vorbisComments, pictureFrames); } /** Returns the maximum size for a decoded frame from the FLAC stream. */ - public int maxDecodedFrameSize() { - return maxBlockSize * channels * (bitsPerSample / 8); + public int getMaxDecodedFrameSize() { + return maxBlockSizeSamples * channels * (bitsPerSample / 8); } /** Returns the bit-rate of the FLAC stream. */ - public int bitRate() { + public int getBitRate() { return bitsPerSample * sampleRate * channels; } - /** Returns the duration of the FLAC stream in microseconds. */ - public long durationUs() { - return (totalSamples * 1000000L) / sampleRate; + /** + * Returns the duration of the FLAC stream in microseconds, or {@link C#TIME_UNSET} if the total + * number of samples if unknown. + */ + public long getDurationUs() { + return totalSamples == 0 ? C.TIME_UNSET : totalSamples * C.MICROS_PER_SECOND / sampleRate; } /** @@ -125,7 +165,7 @@ public final class FlacStreamMetadata { */ public long getSampleIndex(long timeUs) { long sampleIndex = (timeUs * sampleRate) / C.MICROS_PER_SECOND; - return Util.constrainValue(sampleIndex, 0, totalSamples - 1); + return Util.constrainValue(sampleIndex, /* min= */ 0, totalSamples - 1); } /** Returns the approximate number of bytes per frame for the current FLAC stream. */ @@ -136,14 +176,91 @@ public final class FlacStreamMetadata { } else { // Uses the stream's block-size if it's a known fixed block-size stream, otherwise uses the // default value for FLAC block-size, which is 4096. - long blockSize = (minBlockSize == maxBlockSize && minBlockSize > 0) ? minBlockSize : 4096; - approxBytesPerFrame = (blockSize * channels * bitsPerSample) / 8 + 64; + long blockSizeSamples = + (minBlockSizeSamples == maxBlockSizeSamples && minBlockSizeSamples > 0) + ? minBlockSizeSamples + : 4096; + approxBytesPerFrame = (blockSizeSamples * channels * bitsPerSample) / 8 + 64; } return approxBytesPerFrame; } + /** + * Returns a {@link Format} extracted from the FLAC stream metadata. + * + *

{@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 vorbisComments, List pictureFrames) { if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) { return null; diff --git a/library/core/src/test/assets/flac/bear.flac b/library/core/src/test/assets/flac/bear.flac new file mode 100644 index 0000000000..3e17983fef Binary files /dev/null and b/library/core/src/test/assets/flac/bear.flac differ diff --git a/library/core/src/test/assets/flac/bear.flac.0.dump b/library/core/src/test/assets/flac/bear.flac.0.dump new file mode 100644 index 0000000000..e35dcc2081 --- /dev/null +++ b/library/core/src/test/assets/flac/bear.flac.0.dump @@ -0,0 +1,163 @@ +seekMap: + isSeekable = false + duration = 2741000 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + format: + bitrate = 1536000 + id = null + containerMimeType = null + sampleMimeType = audio/flac + maxInputSize = 5776 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 42, hash 83F6895 + total output bytes = 164431 + sample count = 33 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_no_min_max_frame_size.flac b/library/core/src/test/assets/flac/bear_no_min_max_frame_size.flac new file mode 100644 index 0000000000..fb0c9d207e Binary files /dev/null and b/library/core/src/test/assets/flac/bear_no_min_max_frame_size.flac differ diff --git a/library/core/src/test/assets/flac/bear_no_min_max_frame_size.flac.0.dump b/library/core/src/test/assets/flac/bear_no_min_max_frame_size.flac.0.dump new file mode 100644 index 0000000000..2c394e71b7 --- /dev/null +++ b/library/core/src/test/assets/flac/bear_no_min_max_frame_size.flac.0.dump @@ -0,0 +1,163 @@ +seekMap: + isSeekable = false + duration = 2741000 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + format: + bitrate = 1536000 + id = null + containerMimeType = null + sampleMimeType = audio/flac + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 42, hash 9218FDB7 + total output bytes = 164431 + sample count = 33 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_no_num_samples.flac b/library/core/src/test/assets/flac/bear_no_num_samples.flac new file mode 100644 index 0000000000..03696047cd Binary files /dev/null and b/library/core/src/test/assets/flac/bear_no_num_samples.flac differ diff --git a/library/core/src/test/assets/flac/bear_no_num_samples.flac.0.dump b/library/core/src/test/assets/flac/bear_no_num_samples.flac.0.dump new file mode 100644 index 0000000000..c913738be5 --- /dev/null +++ b/library/core/src/test/assets/flac/bear_no_num_samples.flac.0.dump @@ -0,0 +1,163 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + format: + bitrate = 1536000 + id = null + containerMimeType = null + sampleMimeType = audio/flac + maxInputSize = 5776 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 42, hash 49FA2C21 + total output bytes = 164431 + sample count = 33 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_one_metadata_block.flac b/library/core/src/test/assets/flac/bear_one_metadata_block.flac new file mode 100644 index 0000000000..a3fcb8f114 Binary files /dev/null and b/library/core/src/test/assets/flac/bear_one_metadata_block.flac differ diff --git a/library/core/src/test/assets/flac/bear_one_metadata_block.flac.0.dump b/library/core/src/test/assets/flac/bear_one_metadata_block.flac.0.dump new file mode 100644 index 0000000000..e35dcc2081 --- /dev/null +++ b/library/core/src/test/assets/flac/bear_one_metadata_block.flac.0.dump @@ -0,0 +1,163 @@ +seekMap: + isSeekable = false + duration = 2741000 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + format: + bitrate = 1536000 + id = null + containerMimeType = null + sampleMimeType = audio/flac + maxInputSize = 5776 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 42, hash 83F6895 + total output bytes = 164431 + sample count = 33 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_uncommon_sample_rate.flac b/library/core/src/test/assets/flac/bear_uncommon_sample_rate.flac new file mode 100644 index 0000000000..1909853377 Binary files /dev/null and b/library/core/src/test/assets/flac/bear_uncommon_sample_rate.flac differ diff --git a/library/core/src/test/assets/flac/bear_uncommon_sample_rate.flac.0.dump b/library/core/src/test/assets/flac/bear_uncommon_sample_rate.flac.0.dump new file mode 100644 index 0000000000..6ad50afc29 --- /dev/null +++ b/library/core/src/test/assets/flac/bear_uncommon_sample_rate.flac.0.dump @@ -0,0 +1,139 @@ +seekMap: + isSeekable = false + duration = 2741000 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + format: + bitrate = 1408000 + id = null + containerMimeType = null + sampleMimeType = audio/flac + maxInputSize = 6456 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 44000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 42, hash 7249A1B8 + total output bytes = 144086 + sample count = 27 + sample 0: + time = 0 + flags = 1 + data = length 5415, hash 915DBC66 + sample 1: + time = 104727 + flags = 1 + data = length 5529, hash EFD564F7 + sample 2: + time = 209454 + flags = 1 + data = length 5480, hash ADA922FB + sample 3: + time = 314181 + flags = 1 + data = length 5290, hash 7BCEA5FC + sample 4: + time = 418909 + flags = 1 + data = length 5579, hash DBB36F37 + sample 5: + time = 523636 + flags = 1 + data = length 5423, hash AB53F799 + sample 6: + time = 628363 + flags = 1 + data = length 5583, hash 7243C284 + sample 7: + time = 733090 + flags = 1 + data = length 5547, hash 9DA9C99E + sample 8: + time = 837818 + flags = 1 + data = length 5414, hash 90768345 + sample 9: + time = 942545 + flags = 1 + data = length 5531, hash 1CD2FF67 + sample 10: + time = 1047272 + flags = 1 + data = length 5870, hash A9A5CAEE + sample 11: + time = 1152000 + flags = 1 + data = length 5667, hash 875566A1 + sample 12: + time = 1256727 + flags = 1 + data = length 5614, hash 5573694C + sample 13: + time = 1361454 + flags = 1 + data = length 6456, hash 921F3DE7 + sample 14: + time = 1466181 + flags = 1 + data = length 5817, hash EBECBD16 + sample 15: + time = 1570909 + flags = 1 + data = length 5751, hash 4A7D4C6B + sample 16: + time = 1675636 + flags = 1 + data = length 5620, hash B78F8E8D + sample 17: + time = 1780363 + flags = 1 + data = length 5535, hash 8187C107 + sample 18: + time = 1885090 + flags = 1 + data = length 5517, hash 79FF36CB + sample 19: + time = 1989818 + flags = 1 + data = length 5716, hash 349FC281 + sample 20: + time = 2094545 + flags = 1 + data = length 5556, hash BE97B2CA + sample 21: + time = 2199272 + flags = 1 + data = length 5703, hash 531F9FE3 + sample 22: + time = 2304000 + flags = 1 + data = length 5652, hash 1277485D + sample 23: + time = 2408727 + flags = 1 + data = length 5607, hash 14862CB6 + sample 24: + time = 2513454 + flags = 1 + data = length 5829, hash FCAF2F1C + sample 25: + time = 2618181 + flags = 1 + data = length 2837, hash 10F1716E + sample 26: + time = 2722909 + flags = 1 + data = length 548, hash B46F603C +tracksEnded = true diff --git a/library/core/src/test/assets/flac/bear_with_id3.flac b/library/core/src/test/assets/flac/bear_with_id3.flac new file mode 100644 index 0000000000..fc945f14ad Binary files /dev/null and b/library/core/src/test/assets/flac/bear_with_id3.flac differ diff --git a/library/core/src/test/assets/flac/bear_with_id3.flac.0.dump b/library/core/src/test/assets/flac/bear_with_id3.flac.0.dump new file mode 100644 index 0000000000..e35dcc2081 --- /dev/null +++ b/library/core/src/test/assets/flac/bear_with_id3.flac.0.dump @@ -0,0 +1,163 @@ +seekMap: + isSeekable = false + duration = 2741000 + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + format: + bitrate = 1536000 + id = null + containerMimeType = null + sampleMimeType = audio/flac + maxInputSize = 5776 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 42, hash 83F6895 + total output bytes = 164431 + sample count = 33 + sample 0: + time = 0 + flags = 1 + data = length 5030, hash D2B60530 + sample 1: + time = 85333 + flags = 1 + data = length 5066, hash 4C932A54 + sample 2: + time = 170666 + flags = 1 + data = length 5112, hash 7E5A7B61 + sample 3: + time = 256000 + flags = 1 + data = length 5044, hash 7EF93F13 + sample 4: + time = 341333 + flags = 1 + data = length 4943, hash DE7E27F8 + sample 5: + time = 426666 + flags = 1 + data = length 5121, hash 6D0D0B40 + sample 6: + time = 512000 + flags = 1 + data = length 5068, hash 9924644F + sample 7: + time = 597333 + flags = 1 + data = length 5143, hash 6C34F0CE + sample 8: + time = 682666 + flags = 1 + data = length 5109, hash E3B7BEFB + sample 9: + time = 768000 + flags = 1 + data = length 5129, hash 44111D9B + sample 10: + time = 853333 + flags = 1 + data = length 5031, hash 9D55EA53 + sample 11: + time = 938666 + flags = 1 + data = length 5119, hash E1CB9BA6 + sample 12: + time = 1024000 + flags = 1 + data = length 5360, hash 17265C5D + sample 13: + time = 1109333 + flags = 1 + data = length 5340, hash A90FDDF1 + sample 14: + time = 1194666 + flags = 1 + data = length 5162, hash 31F65AD5 + sample 15: + time = 1280000 + flags = 1 + data = length 5168, hash F2394F2D + sample 16: + time = 1365333 + flags = 1 + data = length 5776, hash 58437AB3 + sample 17: + time = 1450666 + flags = 1 + data = length 5394, hash EBAB20A8 + sample 18: + time = 1536000 + flags = 1 + data = length 5168, hash BF37C7A5 + sample 19: + time = 1621333 + flags = 1 + data = length 5324, hash 59546B7B + sample 20: + time = 1706666 + flags = 1 + data = length 5172, hash 6036EF0B + sample 21: + time = 1792000 + flags = 1 + data = length 5102, hash 5A131071 + sample 22: + time = 1877333 + flags = 1 + data = length 5111, hash 3D9EBB3B + sample 23: + time = 1962666 + flags = 1 + data = length 5113, hash 61101D4F + sample 24: + time = 2048000 + flags = 1 + data = length 5229, hash D2E55742 + sample 25: + time = 2133333 + flags = 1 + data = length 5162, hash 7F2E97FA + sample 26: + time = 2218666 + flags = 1 + data = length 5255, hash D92A782 + sample 27: + time = 2304000 + flags = 1 + data = length 5196, hash 98FE5138 + sample 28: + time = 2389333 + flags = 1 + data = length 5214, hash 3D35C38C + sample 29: + time = 2474666 + flags = 1 + data = length 5211, hash 7E25420F + sample 30: + time = 2560000 + flags = 1 + data = length 5230, hash 2AD96FBC + sample 31: + time = 2645333 + flags = 1 + data = length 3384, hash 938BCDD9 + sample 32: + time = 2730666 + flags = 1 + data = length 445, hash A388E3D6 +tracksEnded = true diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java index 43ff7d8f51..ace30dbaf5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; 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; @@ -64,7 +65,8 @@ public final class DefaultExtractorsFactoryTest { PsExtractor.class, WavExtractor.class, AmrExtractor.class, - Ac4Extractor.class + Ac4Extractor.class, + FlacExtractor.class }; assertThat(listCreatedExtractorClasses).containsNoDuplicates(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java new file mode 100644 index 0000000000..3aac12a1a3 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java @@ -0,0 +1,56 @@ +/* + * 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 androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link FlacExtractor}. */ +@RunWith(AndroidJUnit4.class) +public class FlacExtractorTest { + + @Test + public void testSample() throws Exception { + ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear.flac"); + } + + @Test + public void testSampleWithId3() throws Exception { + ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_with_id3.flac"); + } + + @Test + public void testOneMetadataBlock() throws Exception { + ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_one_metadata_block.flac"); + } + + @Test + public void testNoMinMaxFrameSize() throws Exception { + ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_no_min_max_frame_size.flac"); + } + + @Test + public void testNoNumSamples() throws Exception { + ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_no_num_samples.flac"); + } + + @Test + public void testUncommonSampleRate() throws Exception { + ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_uncommon_sample_rate.flac"); + } +}