summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java312
1 files changed, 312 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java
new file mode 100644
index 0000000000..c5413cf459
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil.CommentHeader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac.PictureFrame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Reads and peeks FLAC stream metadata elements according to the <a
+ * href="https://xiph.org/flac/format.html">FLAC format specification</a>.
+ */
+public final class FlacMetadataReader {
+
+ /** Holds a {@link FlacStreamMetadata}. */
+ public static final class FlacStreamMetadataHolder {
+ /** The FLAC stream metadata. */
+ @Nullable public FlacStreamMetadata flacStreamMetadata;
+
+ public FlacStreamMetadataHolder(@Nullable FlacStreamMetadata flacStreamMetadata) {
+ this.flacStreamMetadata = flacStreamMetadata;
+ }
+ }
+
+ private static final int STREAM_MARKER = 0x664C6143; // ASCII for "fLaC"
+ private static final int SYNC_CODE = 0x3FFE;
+ private static final int SEEK_POINT_SIZE = 18;
+
+ /**
+ * Peeks ID3 Data.
+ *
+ * @param input Input stream to peek the ID3 data from.
+ * @param parseData Whether to parse the ID3 frames.
+ * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData}
+ * is {@code false}.
+ * @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.
+ */
+ @Nullable
+ public static Metadata peekId3Metadata(ExtractorInput input, boolean parseData)
+ throws IOException, InterruptedException {
+ @Nullable
+ Id3Decoder.FramePredicate id3FramePredicate = parseData ? null : Id3Decoder.NO_FRAMES_PREDICATE;
+ @Nullable Metadata id3Metadata = new Id3Peeker().peekId3Data(input, id3FramePredicate);
+ return id3Metadata == null || id3Metadata.length() == 0 ? null : id3Metadata;
+ }
+
+ /**
+ * 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, 0, FlacConstants.STREAM_MARKER_SIZE);
+ return scratch.readUnsignedInt() == STREAM_MARKER;
+ }
+
+ /**
+ * Reads ID3 Data.
+ *
+ * <p>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.
+ * @param parseData Whether to parse the ID3 frames.
+ * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData}
+ * is {@code false}.
+ * @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.
+ */
+ @Nullable
+ public static Metadata readId3Metadata(ExtractorInput input, boolean parseData)
+ throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ long startingPeekPosition = input.getPeekPosition();
+ @Nullable Metadata id3Metadata = peekId3Metadata(input, parseData);
+ int peekedId3Bytes = (int) (input.getPeekPosition() - startingPeekPosition);
+ input.skipFully(peekedId3Bytes);
+ return id3Metadata;
+ }
+
+ /**
+ * Reads the FLAC stream marker.
+ *
+ * @param input Input stream to read the stream marker from.
+ * @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)
+ throws IOException, InterruptedException {
+ ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE);
+ input.readFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE);
+ if (scratch.readUnsignedInt() != STREAM_MARKER) {
+ throw new ParserException("Failed to read FLAC stream marker.");
+ }
+ }
+
+ /**
+ * Reads one FLAC metadata block.
+ *
+ * <p>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 block from (header included).
+ * @param metadataHolder A holder for the metadata read. If the stream info block (which must be
+ * the first metadata block) is read, the holder contains a new instance representing the
+ * stream info data. If the block read is a Vorbis comment block or a picture block, the
+ * holder contains a copy of the existing stream metadata with the corresponding metadata
+ * added. Otherwise, the metadata in the holder is unchanged.
+ * @return Whether the block read is the last metadata block.
+ * @throws IllegalArgumentException If the block read is not a stream info block and the metadata
+ * in {@code metadataHolder} is {@code null}. 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 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 boolean readMetadataBlock(
+ ExtractorInput input, FlacStreamMetadataHolder metadataHolder)
+ throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ ParsableBitArray scratch = new ParsableBitArray(new byte[4]);
+ input.peekFully(scratch.data, 0, FlacConstants.METADATA_BLOCK_HEADER_SIZE);
+
+ boolean isLastMetadataBlock = scratch.readBit();
+ int type = scratch.readBits(7);
+ int length = FlacConstants.METADATA_BLOCK_HEADER_SIZE + scratch.readBits(24);
+ if (type == FlacConstants.METADATA_TYPE_STREAM_INFO) {
+ metadataHolder.flacStreamMetadata = readStreamInfoBlock(input);
+ } else {
+ FlacStreamMetadata flacStreamMetadata = metadataHolder.flacStreamMetadata;
+ if (flacStreamMetadata == null) {
+ throw new IllegalArgumentException();
+ }
+ if (type == FlacConstants.METADATA_TYPE_SEEK_TABLE) {
+ FlacStreamMetadata.SeekTable seekTable = readSeekTableMetadataBlock(input, length);
+ metadataHolder.flacStreamMetadata = flacStreamMetadata.copyWithSeekTable(seekTable);
+ } else if (type == FlacConstants.METADATA_TYPE_VORBIS_COMMENT) {
+ List<String> vorbisComments = readVorbisCommentMetadataBlock(input, length);
+ metadataHolder.flacStreamMetadata =
+ flacStreamMetadata.copyWithVorbisComments(vorbisComments);
+ } else if (type == FlacConstants.METADATA_TYPE_PICTURE) {
+ PictureFrame pictureFrame = readPictureMetadataBlock(input, length);
+ metadataHolder.flacStreamMetadata =
+ flacStreamMetadata.copyWithPictureFrames(Collections.singletonList(pictureFrame));
+ } else {
+ input.skipFully(length);
+ }
+ }
+
+ return isLastMetadataBlock;
+ }
+
+ /**
+ * Reads a FLAC seek table metadata block.
+ *
+ * <p>The position of {@code data} is moved to the byte following the seek table metadata block
+ * (placeholder points included).
+ *
+ * @param data The array to read the data from, whose position must correspond to the seek table
+ * metadata block (header included).
+ * @return The seek table, without the placeholder points.
+ */
+ public static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock(ParsableByteArray data) {
+ data.skipBytes(1);
+ int length = data.readUnsignedInt24();
+
+ long seekTableEndPosition = data.getPosition() + length;
+ int seekPointCount = length / SEEK_POINT_SIZE;
+ long[] pointSampleNumbers = new long[seekPointCount];
+ long[] pointOffsets = new long[seekPointCount];
+ for (int i = 0; i < seekPointCount; i++) {
+ // The sample number is expected to fit in a signed long, except if it is a placeholder, in
+ // which case its value is -1.
+ long sampleNumber = data.readLong();
+ if (sampleNumber == -1) {
+ pointSampleNumbers = Arrays.copyOf(pointSampleNumbers, i);
+ pointOffsets = Arrays.copyOf(pointOffsets, i);
+ break;
+ }
+ pointSampleNumbers[i] = sampleNumber;
+ pointOffsets[i] = data.readLong();
+ data.skipBytes(2);
+ }
+
+ data.skipBytes((int) (seekTableEndPosition - data.getPosition()));
+ return new FlacStreamMetadata.SeekTable(pointSampleNumbers, pointOffsets);
+ }
+
+ /**
+ * Returns the frame start marker, consisting of the 2 first bytes of the first frame.
+ *
+ * <p>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 start marker from (starting from the read position).
+ * @return The frame start marker (which must be the same for all the frames in the stream).
+ * @throws ParserException If an error occurs parsing the frame start marker.
+ * @throws IOException If peeking from the input fails.
+ * @throws InterruptedException If interrupted while peeking from input.
+ */
+ public static int getFrameStartMarker(ExtractorInput input)
+ throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ ParsableByteArray scratch = new ParsableByteArray(2);
+ input.peekFully(scratch.data, 0, 2);
+
+ 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.");
+ }
+
+ input.resetPeekPosition();
+ return frameStartMarker;
+ }
+
+ private static FlacStreamMetadata readStreamInfoBlock(ExtractorInput input)
+ throws IOException, InterruptedException {
+ byte[] scratchData = new byte[FlacConstants.STREAM_INFO_BLOCK_SIZE];
+ input.readFully(scratchData, 0, FlacConstants.STREAM_INFO_BLOCK_SIZE);
+ return new FlacStreamMetadata(
+ scratchData, /* offset= */ FlacConstants.METADATA_BLOCK_HEADER_SIZE);
+ }
+
+ private static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock(
+ ExtractorInput input, int length) throws IOException, InterruptedException {
+ ParsableByteArray scratch = new ParsableByteArray(length);
+ input.readFully(scratch.data, 0, length);
+ return readSeekTableMetadataBlock(scratch);
+ }
+
+ private static List<String> readVorbisCommentMetadataBlock(ExtractorInput input, int length)
+ throws IOException, InterruptedException {
+ ParsableByteArray scratch = new ParsableByteArray(length);
+ input.readFully(scratch.data, 0, length);
+ scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);
+ CommentHeader commentHeader =
+ VorbisUtil.readVorbisCommentHeader(
+ scratch, /* hasMetadataHeader= */ false, /* hasFramingBit= */ false);
+ return Arrays.asList(commentHeader.comments);
+ }
+
+ private static PictureFrame readPictureMetadataBlock(ExtractorInput input, int length)
+ throws IOException, InterruptedException {
+ ParsableByteArray scratch = new ParsableByteArray(length);
+ input.readFully(scratch.data, 0, length);
+ scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);
+
+ int pictureType = scratch.readInt();
+ int mimeTypeLength = scratch.readInt();
+ String mimeType = scratch.readString(mimeTypeLength, Charset.forName(C.ASCII_NAME));
+ int descriptionLength = scratch.readInt();
+ String description = scratch.readString(descriptionLength);
+ int width = scratch.readInt();
+ int height = scratch.readInt();
+ int depth = scratch.readInt();
+ int colors = scratch.readInt();
+ int pictureDataLength = scratch.readInt();
+ byte[] pictureData = new byte[pictureDataLength];
+ scratch.readBytes(pictureData, 0, pictureDataLength);
+
+ return new PictureFrame(
+ pictureType, mimeType, description, width, height, depth, colors, pictureData);
+ }
+
+ private FlacMetadataReader() {}
+}