blob: fdb36a2771c4f7e7350380e65df8edca2818ba26 [file] [log] [blame]
/*
* 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;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.VorbisUtil.CommentHeader;
import com.google.android.exoplayer2.extractor.flac.FlacConstants;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.common.base.Charsets;
import java.io.IOException;
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.
*/
@Nullable
public static Metadata peekId3Metadata(ExtractorInput input, boolean parseData)
throws IOException {
@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.
*/
public static boolean checkAndPeekStreamMarker(ExtractorInput input) throws IOException {
ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE);
input.peekFully(scratch.getData(), 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.
*/
@Nullable
public static Metadata readId3Metadata(ExtractorInput input, boolean parseData)
throws IOException {
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.
*/
public static void readStreamMarker(ExtractorInput input) throws IOException {
ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE);
input.readFully(scratch.getData(), 0, FlacConstants.STREAM_MARKER_SIZE);
if (scratch.readUnsignedInt() != STREAM_MARKER) {
throw ParserException.createForMalformedContainer(
"Failed to read FLAC stream marker.", /* cause= */ null);
}
}
/**
* 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.
*/
public static boolean readMetadataBlock(
ExtractorInput input, FlacStreamMetadataHolder metadataHolder) throws IOException {
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 {
@Nullable 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 = (long) 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.
*/
public static int getFrameStartMarker(ExtractorInput input) throws IOException {
input.resetPeekPosition();
ParsableByteArray scratch = new ParsableByteArray(2);
input.peekFully(scratch.getData(), 0, 2);
int frameStartMarker = scratch.readUnsignedShort();
int syncCode = frameStartMarker >> 2;
if (syncCode != SYNC_CODE) {
input.resetPeekPosition();
throw ParserException.createForMalformedContainer(
"First frame does not start with sync code.", /* cause= */ null);
}
input.resetPeekPosition();
return frameStartMarker;
}
private static FlacStreamMetadata readStreamInfoBlock(ExtractorInput input) throws IOException {
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 {
ParsableByteArray scratch = new ParsableByteArray(length);
input.readFully(scratch.getData(), 0, length);
return readSeekTableMetadataBlock(scratch);
}
private static List<String> readVorbisCommentMetadataBlock(ExtractorInput input, int length)
throws IOException {
ParsableByteArray scratch = new ParsableByteArray(length);
input.readFully(scratch.getData(), 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 {
ParsableByteArray scratch = new ParsableByteArray(length);
input.readFully(scratch.getData(), 0, length);
scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);
int pictureType = scratch.readInt();
int mimeTypeLength = scratch.readInt();
String mimeType = scratch.readString(mimeTypeLength, Charsets.US_ASCII);
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() {}
}