Expose metadata in FLAC extractor

PiperOrigin-RevId: 281538423
This commit is contained in:
kimvde 2019-11-20 17:40:03 +00:00 committed by Oliver Woodman
parent f6853e4751
commit b18650fdcf
32 changed files with 1028 additions and 1455 deletions

View File

@ -4,9 +4,9 @@
* Add Java FLAC extractor * Add Java FLAC extractor
([#6406](https://github.com/google/ExoPlayer/issues/6406)). ([#6406](https://github.com/google/ExoPlayer/issues/6406)).
This extractor does not support seeking and live streams, and does not expose This extractor does not support seeking and live streams. If
vorbis, ID3 and picture data. If `DefaultExtractorsFactory` is used, this `DefaultExtractorsFactory` is used, this extractor is only used if the FLAC
extractor is only used if the FLAC extension is not loaded. extension is not loaded.
* Video tunneling: Fix renderer end-of-stream with `OnFrameRenderedListener` * Video tunneling: Fix renderer end-of-stream with `OnFrameRenderedListener`
from API 23, tunneled renderer must send a special timestamp on EOS. from API 23, tunneled renderer must send a special timestamp on EOS.
Previously the EOS was reported when the input stream reached EOS. Previously the EOS was reported when the input stream reached EOS.

View File

@ -26,15 +26,13 @@ import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.Id3Peeker; import com.google.android.exoplayer2.extractor.FlacMetadataReader;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata; 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.Assertions;
import com.google.android.exoplayer2.util.FlacMetadataReader;
import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
@ -73,7 +71,6 @@ public final class FlacExtractor implements Extractor {
public static final int FLAG_DISABLE_ID3_METADATA = 1; public static final int FLAG_DISABLE_ID3_METADATA = 1;
private final ParsableByteArray outputBuffer; private final ParsableByteArray outputBuffer;
private final Id3Peeker id3Peeker;
private final boolean id3MetadataDisabled; private final boolean id3MetadataDisabled;
@Nullable private FlacDecoderJni decoderJni; @Nullable private FlacDecoderJni decoderJni;
@ -87,7 +84,7 @@ public final class FlacExtractor implements Extractor {
@Nullable private Metadata id3Metadata; @Nullable private Metadata id3Metadata;
@Nullable private FlacBinarySearchSeeker binarySearchSeeker; @Nullable private FlacBinarySearchSeeker binarySearchSeeker;
/** Constructs an instance with flags = 0. */ /** Constructs an instance with {@code flags = 0}. */
public FlacExtractor() { public FlacExtractor() {
this(/* flags= */ 0); this(/* flags= */ 0);
} }
@ -95,11 +92,11 @@ public final class FlacExtractor implements Extractor {
/** /**
* Constructs an instance. * Constructs an instance.
* *
* @param flags Flags that control the extractor's behavior. * @param flags Flags that control the extractor's behavior. Possible flags are described by
* {@link Flags}.
*/ */
public FlacExtractor(int flags) { public FlacExtractor(int flags) {
outputBuffer = new ParsableByteArray(); outputBuffer = new ParsableByteArray();
id3Peeker = new Id3Peeker();
id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
} }
@ -117,7 +114,7 @@ public final class FlacExtractor implements Extractor {
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
id3Metadata = peekId3Data(input); id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ !id3MetadataDisabled);
return FlacMetadataReader.checkAndPeekStreamMarker(input); return FlacMetadataReader.checkAndPeekStreamMarker(input);
} }
@ -125,7 +122,7 @@ public final class FlacExtractor implements Extractor {
public int read(final ExtractorInput input, PositionHolder seekPosition) public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) { if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
id3Metadata = peekId3Data(input); id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true);
} }
FlacDecoderJni decoderJni = initDecoderJni(input); FlacDecoderJni decoderJni = initDecoderJni(input);
@ -177,19 +174,6 @@ public final class FlacExtractor implements Extractor {
} }
} }
/**
* Peeks ID3 tag data at the beginning of the input.
*
* @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input.
*/
@Nullable
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition();
Id3Decoder.FramePredicate id3FramePredicate =
id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
return id3Peeker.peekId3Data(input, id3FramePredicate);
}
@EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized. @EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized.
@SuppressWarnings({"contracts.postcondition.not.satisfied"}) @SuppressWarnings({"contracts.postcondition.not.satisfied"})
private FlacDecoderJni initDecoderJni(ExtractorInput input) { private FlacDecoderJni initDecoderJni(ExtractorInput input) {
@ -220,10 +204,7 @@ public final class FlacExtractor implements Extractor {
this.streamMetadata = streamMetadata; this.streamMetadata = streamMetadata;
binarySearchSeeker = binarySearchSeeker =
outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput); outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput);
Metadata metadata = id3MetadataDisabled ? null : id3Metadata; Metadata metadata = streamMetadata.getMetadataCopyWithAppendedEntriesFrom(id3Metadata);
if (streamMetadata.metadata != null) {
metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata);
}
outputFormat(streamMetadata, metadata, trackOutput); outputFormat(streamMetadata, metadata, trackOutput);
outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize()); outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize());
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));

View File

@ -151,7 +151,7 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
"FlacStreamMetadata"); "FlacStreamMetadata");
jmethodID flacStreamMetadataConstructor = jmethodID flacStreamMetadataConstructor =
env->GetMethodID(flacStreamMetadataClass, "<init>", env->GetMethodID(flacStreamMetadataClass, "<init>",
"(IIIIIIIJLjava/util/List;Ljava/util/List;)V"); "(IIIIIIIJLjava/util/ArrayList;Ljava/util/ArrayList;)V");
return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor, return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
streamInfo.min_blocksize, streamInfo.max_blocksize, streamInfo.min_blocksize, streamInfo.max_blocksize,

View File

@ -13,7 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
/** Reads and peeks FLAC frame elements. */ /** Reads and peeks FLAC frame elements. */
public final class FlacFrameReader { public final class FlacFrameReader {

View File

@ -0,0 +1,281 @@
/*
* 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.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.VorbisUtil.CommentHeader;
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.FlacConstants;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.ParsableBitArray;
import 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 from an {@link ExtractorInput}. */
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;
}
}
/** 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;
private static final int STREAM_INFO_TYPE = 0;
private static final int VORBIS_COMMENT_TYPE = 4;
private static final int PICTURE_TYPE = 6;
/**
* 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;
return new Id3Peeker().peekId3Data(input, id3FramePredicate);
}
/**
* 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 == STREAM_INFO_TYPE) {
metadataHolder.flacStreamMetadata = readStreamInfoBlock(input);
} else {
FlacStreamMetadata flacStreamMetadata = metadataHolder.flacStreamMetadata;
if (flacStreamMetadata == null) {
throw new IllegalArgumentException();
}
if (type == VORBIS_COMMENT_TYPE) {
List<String> vorbisComments = readVorbisCommentMetadataBlock(input, length);
metadataHolder.flacStreamMetadata =
flacStreamMetadata.copyWithVorbisComments(vorbisComments);
} else if (type == PICTURE_TYPE) {
PictureFrame pictureFrame = readPictureMetadataBlock(input, length);
metadataHolder.flacStreamMetadata =
flacStreamMetadata.copyWithPictureFrames(Collections.singletonList(pictureFrame));
} else {
input.skipFully(length);
}
}
return isLastMetadataBlock;
}
/**
* Returns some metadata extracted from the first frame of a FLAC stream.
*
* <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 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(FlacConstants.MAX_FRAME_HEADER_SIZE);
input.peekFully(scratch.data, 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 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 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() {}
}

View File

@ -13,17 +13,17 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.extractor.ogg; package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
/** /**
* Wraps a byte array, providing methods that allow it to be read as a vorbis bitstream. * Wraps a byte array, providing methods that allow it to be read as a Vorbis bitstream.
* *
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-360002">Vorbis bitpacking * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-360002">Vorbis bitpacking
* specification</a> * specification</a>
*/ */
/* package */ final class VorbisBitArray { public final class VorbisBitArray {
private final byte[] data; private final byte[] data;
private final int byteLimit; private final int byteLimit;

View File

@ -13,17 +13,87 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.extractor.ogg; package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.Arrays; import java.util.Arrays;
/** /** Utility methods for parsing Vorbis streams. */
* Utility methods for parsing vorbis streams. public final class VorbisUtil {
*/
/* package */ final class VorbisUtil { /** Vorbis comment header. */
public static final class CommentHeader {
public final String vendor;
public final String[] comments;
public final int length;
public CommentHeader(String vendor, String[] comments, int length) {
this.vendor = vendor;
this.comments = comments;
this.length = length;
}
}
/** Vorbis identification header. */
public static final class VorbisIdHeader {
public final long version;
public final int channels;
public final long sampleRate;
public final int bitrateMax;
public final int bitrateNominal;
public final int bitrateMin;
public final int blockSize0;
public final int blockSize1;
public final boolean framingFlag;
public final byte[] data;
public VorbisIdHeader(
long version,
int channels,
long sampleRate,
int bitrateMax,
int bitrateNominal,
int bitrateMin,
int blockSize0,
int blockSize1,
boolean framingFlag,
byte[] data) {
this.version = version;
this.channels = channels;
this.sampleRate = sampleRate;
this.bitrateMax = bitrateMax;
this.bitrateNominal = bitrateNominal;
this.bitrateMin = bitrateMin;
this.blockSize0 = blockSize0;
this.blockSize1 = blockSize1;
this.framingFlag = framingFlag;
this.data = data;
}
public int getApproximateBitrate() {
return bitrateNominal == 0 ? (bitrateMin + bitrateMax) / 2 : bitrateNominal;
}
}
/** Vorbis setup header modes. */
public static final class Mode {
public final boolean blockFlag;
public final int windowType;
public final int transformType;
public final int mapping;
public Mode(boolean blockFlag, int windowType, int transformType, int mapping) {
this.blockFlag = blockFlag;
this.windowType = windowType;
this.transformType = transformType;
this.mapping = mapping;
}
}
private static final String TAG = "VorbisUtil"; private static final String TAG = "VorbisUtil";
@ -45,7 +115,7 @@ import java.util.Arrays;
} }
/** /**
* Reads a vorbis identification header from {@code headerData}. * Reads a Vorbis identification header from {@code headerData}.
* *
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2">Vorbis * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2">Vorbis
* spec/Identification header</a> * spec/Identification header</a>
@ -70,7 +140,7 @@ import java.util.Arrays;
int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4); int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4);
boolean framingFlag = (headerData.readUnsignedByte() & 0x01) > 0; boolean framingFlag = (headerData.readUnsignedByte() & 0x01) > 0;
// raw data of vorbis setup header has to be passed to decoder as CSD buffer #1 // raw data of Vorbis setup header has to be passed to decoder as CSD buffer #1
byte[] data = Arrays.copyOf(headerData.data, headerData.limit()); byte[] data = Arrays.copyOf(headerData.data, headerData.limit());
return new VorbisIdHeader(version, channels, sampleRate, bitrateMax, bitrateNominal, bitrateMin, return new VorbisIdHeader(version, channels, sampleRate, bitrateMax, bitrateNominal, bitrateMin,
@ -78,18 +148,41 @@ import java.util.Arrays;
} }
/** /**
* Reads a vorbis comment header. * Reads a Vorbis comment header.
* *
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3"> * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3">Vorbis
* Vorbis spec/Comment header</a> * spec/Comment header</a>
* @param headerData a {@link ParsableByteArray} wrapping the header data. * @param headerData A {@link ParsableByteArray} wrapping the header data.
* @return a {@link VorbisUtil.CommentHeader} with all the comments. * @return A {@link VorbisUtil.CommentHeader} with all the comments.
* @throws ParserException thrown if invalid capture pattern is detected. * @throws ParserException If an error occurs parsing the comment header.
*/ */
public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData) public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData)
throws ParserException { throws ParserException {
return readVorbisCommentHeader(
headerData, /* hasMetadataHeader= */ true, /* hasFramingBit= */ true);
}
verifyVorbisHeaderCapturePattern(0x03, headerData, false); /**
* Reads a Vorbis comment header.
*
* <p>The data provided may not contain the Vorbis metadata common header and the framing bit.
*
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3">Vorbis
* spec/Comment header</a>
* @param headerData A {@link ParsableByteArray} wrapping the header data.
* @param hasMetadataHeader Whether the {@code headerData} contains a Vorbis metadata common
* header preceding the comment header.
* @param hasFramingBit Whether the {@code headerData} contains a framing bit.
* @return A {@link VorbisUtil.CommentHeader} with all the comments.
* @throws ParserException If an error occurs parsing the comment header.
*/
public static CommentHeader readVorbisCommentHeader(
ParsableByteArray headerData, boolean hasMetadataHeader, boolean hasFramingBit)
throws ParserException {
if (hasMetadataHeader) {
verifyVorbisHeaderCapturePattern(/* headerType= */ 0x03, headerData, /* quiet= */ false);
}
int length = 7; int length = 7;
int len = (int) headerData.readLittleEndianUnsignedInt(); int len = (int) headerData.readLittleEndianUnsignedInt();
@ -106,7 +199,7 @@ import java.util.Arrays;
comments[i] = headerData.readString(len); comments[i] = headerData.readString(len);
length += comments[i].length(); length += comments[i].length();
} }
if ((headerData.readUnsignedByte() & 0x01) == 0) { if (hasFramingBit && (headerData.readUnsignedByte() & 0x01) == 0) {
throw new ParserException("framing bit expected to be set"); throw new ParserException("framing bit expected to be set");
} }
length += 1; length += 1;
@ -114,8 +207,8 @@ import java.util.Arrays;
} }
/** /**
* Verifies whether the next bytes in {@code header} are a vorbis header of the given * Verifies whether the next bytes in {@code header} are a Vorbis header of the given {@code
* {@code headerType}. * headerType}.
* *
* @param headerType the type of the header expected. * @param headerType the type of the header expected.
* @param header the alleged header bytes. * @param header the alleged header bytes.
@ -123,9 +216,8 @@ import java.util.Arrays;
* @return the number of bytes read. * @return the number of bytes read.
* @throws ParserException thrown if header type or capture pattern is not as expected. * @throws ParserException thrown if header type or capture pattern is not as expected.
*/ */
public static boolean verifyVorbisHeaderCapturePattern(int headerType, ParsableByteArray header, public static boolean verifyVorbisHeaderCapturePattern(
boolean quiet) int headerType, ParsableByteArray header, boolean quiet) throws ParserException {
throws ParserException {
if (header.bytesLeft() < 7) { if (header.bytesLeft() < 7) {
if (quiet) { if (quiet) {
return false; return false;
@ -158,12 +250,12 @@ import java.util.Arrays;
} }
/** /**
* This method reads the modes which are located at the very end of the vorbis setup header. * This method reads the modes which are located at the very end of the Vorbis setup header.
* That's why we need to partially decode or at least read the entire setup header to know * That's why we need to partially decode or at least read the entire setup header to know where
* where to start reading the modes. * to start reading the modes.
* *
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-650004.2.4"> * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-650004.2.4">Vorbis
* Vorbis spec/Setup header</a> * spec/Setup header</a>
* @param headerData a {@link ParsableByteArray} containing setup header data. * @param headerData a {@link ParsableByteArray} containing setup header data.
* @param channels the number of channels. * @param channels the number of channels.
* @return an array of {@link Mode}s. * @return an array of {@link Mode}s.
@ -409,7 +501,7 @@ import java.util.Arrays;
// Prevent instantiation. // Prevent instantiation.
} }
public static final class CodeBook { private static final class CodeBook {
public final int dimensions; public final int dimensions;
public final int entries; public final int entries;
@ -427,69 +519,4 @@ import java.util.Arrays;
} }
} }
public static final class CommentHeader {
public final String vendor;
public final String[] comments;
public final int length;
public CommentHeader(String vendor, String[] comments, int length) {
this.vendor = vendor;
this.comments = comments;
this.length = length;
}
}
public static final class VorbisIdHeader {
public final long version;
public final int channels;
public final long sampleRate;
public final int bitrateMax;
public final int bitrateNominal;
public final int bitrateMin;
public final int blockSize0;
public final int blockSize1;
public final boolean framingFlag;
public final byte[] data;
public VorbisIdHeader(long version, int channels, long sampleRate, int bitrateMax,
int bitrateNominal, int bitrateMin, int blockSize0, int blockSize1, boolean framingFlag,
byte[] data) {
this.version = version;
this.channels = channels;
this.sampleRate = sampleRate;
this.bitrateMax = bitrateMax;
this.bitrateNominal = bitrateNominal;
this.bitrateMin = bitrateMin;
this.blockSize0 = blockSize0;
this.blockSize1 = blockSize1;
this.framingFlag = framingFlag;
this.data = data;
}
public int getApproximateBitrate() {
return bitrateNominal == 0 ? (bitrateMin + bitrateMax) / 2 : bitrateNominal;
}
}
public static final class Mode {
public final boolean blockFlag;
public final int windowType;
public final int transformType;
public final int mapping;
public Mode(boolean blockFlag, int windowType, int transformType, int mapping) {
this.blockFlag = blockFlag;
this.windowType = windowType;
this.transformType = transformType;
this.mapping = mapping;
}
}
} }

View File

@ -18,20 +18,22 @@ package com.google.android.exoplayer2.extractor.flac;
import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.android.exoplayer2.util.Util.castNonNull;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.FlacFrameReader;
import com.google.android.exoplayer2.extractor.FlacFrameReader.BlockSizeHolder;
import com.google.android.exoplayer2.extractor.FlacMetadataReader;
import com.google.android.exoplayer2.extractor.FlacMetadataReader.FirstFrameMetadata;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacConstants; 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.FlacStreamMetadata;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
@ -41,7 +43,6 @@ import java.lang.annotation.RetentionPolicy;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// TODO: implement seeking. // TODO: implement seeking.
// TODO: expose vorbis and ID3 data.
// TODO: support live streams. // TODO: support live streams.
/** /**
* Extracts data from FLAC container format. * Extracts data from FLAC container format.
@ -53,23 +54,40 @@ public final class FlacExtractor implements Extractor {
/** Factory for {@link FlacExtractor} instances. */ /** Factory for {@link FlacExtractor} instances. */
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()}; public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};
/**
* Flags controlling the behavior of the extractor. Possible flag value is {@link
* #FLAG_DISABLE_ID3_METADATA}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {FLAG_DISABLE_ID3_METADATA})
public @interface Flags {}
/**
* Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
* required.
*/
public static final int FLAG_DISABLE_ID3_METADATA = 1;
/** Parser state. */ /** Parser state. */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({ @IntDef({
STATE_READ_ID3_TAG, STATE_READ_ID3_METADATA,
STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES,
STATE_READ_STREAM_MARKER, STATE_READ_STREAM_MARKER,
STATE_READ_STREAM_INFO_BLOCK, STATE_READ_METADATA_BLOCKS,
STATE_SKIP_OPTIONAL_METADATA_BLOCKS,
STATE_GET_FIRST_FRAME_METADATA, STATE_GET_FIRST_FRAME_METADATA,
STATE_READ_FRAMES STATE_READ_FRAMES
}) })
private @interface State {} private @interface State {}
private static final int STATE_READ_ID3_TAG = 0; private static final int STATE_READ_ID3_METADATA = 0;
private static final int STATE_READ_STREAM_MARKER = 1; private static final int STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES = 1;
private static final int STATE_READ_STREAM_INFO_BLOCK = 2; private static final int STATE_READ_STREAM_MARKER = 2;
private static final int STATE_SKIP_OPTIONAL_METADATA_BLOCKS = 3; private static final int STATE_READ_METADATA_BLOCKS = 3;
private static final int STATE_GET_FIRST_FRAME_METADATA = 4; private static final int STATE_GET_FIRST_FRAME_METADATA = 4;
private static final int STATE_READ_FRAMES = 5; private static final int STATE_READ_FRAMES = 5;
@ -81,6 +99,7 @@ public final class FlacExtractor implements Extractor {
private final byte[] streamMarkerAndInfoBlock; private final byte[] streamMarkerAndInfoBlock;
private final ParsableByteArray scratch; private final ParsableByteArray scratch;
private final boolean id3MetadataDisabled;
private final BlockSizeHolder blockSizeHolder; private final BlockSizeHolder blockSizeHolder;
@ -88,6 +107,7 @@ public final class FlacExtractor implements Extractor {
@MonotonicNonNull private TrackOutput trackOutput; @MonotonicNonNull private TrackOutput trackOutput;
private @State int state; private @State int state;
@Nullable private Metadata id3Metadata;
@MonotonicNonNull private FlacStreamMetadata flacStreamMetadata; @MonotonicNonNull private FlacStreamMetadata flacStreamMetadata;
private int minFrameSize; private int minFrameSize;
private int frameStartMarker; private int frameStartMarker;
@ -95,16 +115,28 @@ public final class FlacExtractor implements Extractor {
private int currentFrameBytesWritten; private int currentFrameBytesWritten;
private long totalSamplesWritten; private long totalSamplesWritten;
/** Constructs an instance with {@code flags = 0}. */
public FlacExtractor() { public FlacExtractor() {
this(/* flags= */ 0);
}
/**
* Constructs an instance.
*
* @param flags Flags that control the extractor's behavior. Possible flags are described by
* {@link Flags}.
*/
public FlacExtractor(int flags) {
streamMarkerAndInfoBlock = streamMarkerAndInfoBlock =
new byte[FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE]; new byte[FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE];
scratch = new ParsableByteArray(SCRATCH_LENGTH); scratch = new ParsableByteArray(SCRATCH_LENGTH);
blockSizeHolder = new BlockSizeHolder(); blockSizeHolder = new BlockSizeHolder();
id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
} }
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
FlacMetadataReader.peekId3Data(input); FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false);
return FlacMetadataReader.checkAndPeekStreamMarker(input); return FlacMetadataReader.checkAndPeekStreamMarker(input);
} }
@ -119,17 +151,17 @@ public final class FlacExtractor implements Extractor {
public int read(ExtractorInput input, PositionHolder seekPosition) public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
switch (state) { switch (state) {
case STATE_READ_ID3_TAG: case STATE_READ_ID3_METADATA:
readId3Tag(input); readId3Metadata(input);
return Extractor.RESULT_CONTINUE;
case STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES:
getStreamMarkerAndInfoBlockBytes(input);
return Extractor.RESULT_CONTINUE; return Extractor.RESULT_CONTINUE;
case STATE_READ_STREAM_MARKER: case STATE_READ_STREAM_MARKER:
readStreamMarker(input); readStreamMarker(input);
return Extractor.RESULT_CONTINUE; return Extractor.RESULT_CONTINUE;
case STATE_READ_STREAM_INFO_BLOCK: case STATE_READ_METADATA_BLOCKS:
readStreamInfoBlock(input); readMetadataBlocks(input);
return Extractor.RESULT_CONTINUE;
case STATE_SKIP_OPTIONAL_METADATA_BLOCKS:
skipOptionalMetadataBlocks(input);
return Extractor.RESULT_CONTINUE; return Extractor.RESULT_CONTINUE;
case STATE_GET_FIRST_FRAME_METADATA: case STATE_GET_FIRST_FRAME_METADATA:
getFirstFrameMetadata(input); getFirstFrameMetadata(input);
@ -143,7 +175,7 @@ public final class FlacExtractor implements Extractor {
@Override @Override
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
state = STATE_READ_ID3_TAG; state = STATE_READ_ID3_METADATA;
currentFrameBytesWritten = 0; currentFrameBytesWritten = 0;
totalSamplesWritten = 0; totalSamplesWritten = 0;
scratch.reset(); scratch.reset();
@ -156,40 +188,40 @@ public final class FlacExtractor implements Extractor {
// Private methods. // Private methods.
private void readId3Tag(ExtractorInput input) throws IOException, InterruptedException { private void readId3Metadata(ExtractorInput input) throws IOException, InterruptedException {
FlacMetadataReader.readId3Data(input); id3Metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ !id3MetadataDisabled);
state = STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES;
}
private void getStreamMarkerAndInfoBlockBytes(ExtractorInput input)
throws IOException, InterruptedException {
input.peekFully(streamMarkerAndInfoBlock, 0, streamMarkerAndInfoBlock.length);
input.resetPeekPosition();
state = STATE_READ_STREAM_MARKER; state = STATE_READ_STREAM_MARKER;
} }
private void readStreamMarker(ExtractorInput input) throws IOException, InterruptedException { private void readStreamMarker(ExtractorInput input) throws IOException, InterruptedException {
FlacMetadataReader.readStreamMarker( FlacMetadataReader.readStreamMarker(input);
input, streamMarkerAndInfoBlock, /* scratchWriteIndex= */ 0); state = STATE_READ_METADATA_BLOCKS;
state = STATE_READ_STREAM_INFO_BLOCK;
} }
private void readStreamInfoBlock(ExtractorInput input) throws IOException, InterruptedException { private void readMetadataBlocks(ExtractorInput input) throws IOException, InterruptedException {
flacStreamMetadata = boolean isLastMetadataBlock = false;
FlacMetadataReader.readStreamInfoBlock( FlacMetadataReader.FlacStreamMetadataHolder metadataHolder =
input, new FlacMetadataReader.FlacStreamMetadataHolder(flacStreamMetadata);
/* scratchData= */ streamMarkerAndInfoBlock, while (!isLastMetadataBlock) {
/* scratchWriteIndex= */ FlacConstants.STREAM_MARKER_SIZE); isLastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, metadataHolder);
// Save the current metadata in case an exception occurs.
flacStreamMetadata = castNonNull(metadataHolder.flacStreamMetadata);
}
Assertions.checkNotNull(flacStreamMetadata);
minFrameSize = Math.max(flacStreamMetadata.minFrameSize, FlacConstants.MIN_FRAME_HEADER_SIZE); minFrameSize = Math.max(flacStreamMetadata.minFrameSize, FlacConstants.MIN_FRAME_HEADER_SIZE);
boolean isLastMetadataBlock = castNonNull(trackOutput)
(streamMarkerAndInfoBlock[FlacConstants.STREAM_MARKER_SIZE] >> 7 & 1) == 1; .format(flacStreamMetadata.getFormat(streamMarkerAndInfoBlock, id3Metadata));
castNonNull(trackOutput).format(flacStreamMetadata.getFormat(streamMarkerAndInfoBlock));
castNonNull(extractorOutput) castNonNull(extractorOutput)
.seekMap(new SeekMap.Unseekable(flacStreamMetadata.getDurationUs())); .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; state = STATE_GET_FIRST_FRAME_METADATA;
} }

View File

@ -69,7 +69,7 @@ import java.util.Arrays;
if (streamMetadata == null) { if (streamMetadata == null) {
streamMetadata = new FlacStreamMetadata(data, 17); streamMetadata = new FlacStreamMetadata(data, 17);
byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
setupData.format = streamMetadata.getFormat(metadata); setupData.format = streamMetadata.getFormat(metadata, /* id3Metadata= */ null);
} else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) { } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) {
flacOggSeeker = new FlacOggSeeker(); flacOggSeeker = new FlacOggSeeker();
flacOggSeeker.parseSeekTable(packet); flacOggSeeker.parseSeekTable(packet);

View File

@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.ogg;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ogg.VorbisUtil.Mode; import com.google.android.exoplayer2.extractor.VorbisUtil;
import com.google.android.exoplayer2.extractor.VorbisUtil.Mode;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;

View File

@ -1,208 +0,0 @@
/*
* 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.
*
* <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.
* @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.
*
* <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 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.
*
* <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 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() {}
}

View File

@ -25,13 +25,24 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
/** Holder for FLAC metadata. */ /**
* Holder for FLAC metadata.
*
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a>
* @see <a href="https://xiph.org/flac/format.html#metadata_block_vorbis_comment">FLAC format
* METADATA_BLOCK_VORBIS_COMMENT</a>
* @see <a href="https://xiph.org/flac/format.html#metadata_block_picture">FLAC format
* METADATA_BLOCK_PICTURE</a>
*/
public final class FlacStreamMetadata { public final class FlacStreamMetadata {
private static final String TAG = "FlacStreamMetadata"; private static final String TAG = "FlacStreamMetadata";
/** Indicates that a value is not in the corresponding lookup table. */ /** Indicates that a value is not in the corresponding lookup table. */
public static final int NOT_IN_LOOKUP_TABLE = -1; public static final int NOT_IN_LOOKUP_TABLE = -1;
/** Separator between the field name of a Vorbis comment and the corresponding value. */
private static final String SEPARATOR = "=";
/** Minimum number of samples per block. */ /** Minimum number of samples per block. */
public final int minBlockSizeSamples; public final int minBlockSizeSamples;
@ -68,53 +79,33 @@ public final class FlacStreamMetadata {
public final int bitsPerSampleLookupKey; public final int bitsPerSampleLookupKey;
/** Total number of samples, or 0 if the value is unknown. */ /** Total number of samples, or 0 if the value is unknown. */
public final long totalSamples; public final long totalSamples;
/** Stream content metadata. */
@Nullable public final Metadata metadata;
private static final String SEPARATOR = "="; /** Content metadata. */
private final Metadata metadata;
/** /**
* Parses binary FLAC stream info metadata. * Parses binary FLAC stream info metadata.
* *
* @param data An array containing binary FLAC stream info metadata. * @param data An array containing binary FLAC stream info block (with or without header).
* @param offset The offset of the stream info block in {@code data} (header excluded). * @param offset The offset of the stream info block in {@code data} (header excluded).
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a>
*/ */
public FlacStreamMetadata(byte[] data, int offset) { public FlacStreamMetadata(byte[] data, int offset) {
ParsableBitArray scratch = new ParsableBitArray(data); ParsableBitArray scratch = new ParsableBitArray(data);
scratch.setPosition(offset * 8); scratch.setPosition(offset * 8);
this.minBlockSizeSamples = scratch.readBits(16); minBlockSizeSamples = scratch.readBits(16);
this.maxBlockSizeSamples = scratch.readBits(16); maxBlockSizeSamples = scratch.readBits(16);
this.minFrameSize = scratch.readBits(24); minFrameSize = scratch.readBits(24);
this.maxFrameSize = scratch.readBits(24); maxFrameSize = scratch.readBits(24);
this.sampleRate = scratch.readBits(20); sampleRate = scratch.readBits(20);
this.sampleRateLookupKey = getSampleRateLookupKey(); sampleRateLookupKey = getSampleRateLookupKey(sampleRate);
this.channels = scratch.readBits(3) + 1; channels = scratch.readBits(3) + 1;
this.bitsPerSample = scratch.readBits(5) + 1; bitsPerSample = scratch.readBits(5) + 1;
this.bitsPerSampleLookupKey = getBitsPerSampleLookupKey(); bitsPerSampleLookupKey = getBitsPerSampleLookupKey(bitsPerSample);
this.totalSamples = scratch.readBitsToLong(36); totalSamples = scratch.readBitsToLong(36);
this.metadata = null; metadata = new Metadata();
} }
/** // Used in native code.
* @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.
* @param channels Number of channels of the FLAC stream.
* @param bitsPerSample Number of bits per sample of the FLAC stream.
* @param totalSamples Total samples of the FLAC stream.
* @param vorbisComments Vorbis comments. Each entry must be in key=value form.
* @param pictureFrames Picture frames.
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a>
* @see <a href="https://xiph.org/flac/format.html#metadata_block_vorbis_comment">FLAC format
* METADATA_BLOCK_VORBIS_COMMENT</a>
* @see <a href="https://xiph.org/flac/format.html#metadata_block_picture">FLAC format
* METADATA_BLOCK_PICTURE</a>
*/
public FlacStreamMetadata( public FlacStreamMetadata(
int minBlockSizeSamples, int minBlockSizeSamples,
int maxBlockSizeSamples, int maxBlockSizeSamples,
@ -124,19 +115,41 @@ public final class FlacStreamMetadata {
int channels, int channels,
int bitsPerSample, int bitsPerSample,
long totalSamples, long totalSamples,
List<String> vorbisComments, ArrayList<String> vorbisComments,
List<PictureFrame> pictureFrames) { ArrayList<PictureFrame> pictureFrames) {
this(
minBlockSizeSamples,
maxBlockSizeSamples,
minFrameSize,
maxFrameSize,
sampleRate,
channels,
bitsPerSample,
totalSamples,
buildMetadata(vorbisComments, pictureFrames));
}
private FlacStreamMetadata(
int minBlockSizeSamples,
int maxBlockSizeSamples,
int minFrameSize,
int maxFrameSize,
int sampleRate,
int channels,
int bitsPerSample,
long totalSamples,
Metadata metadata) {
this.minBlockSizeSamples = minBlockSizeSamples; this.minBlockSizeSamples = minBlockSizeSamples;
this.maxBlockSizeSamples = maxBlockSizeSamples; this.maxBlockSizeSamples = maxBlockSizeSamples;
this.minFrameSize = minFrameSize; this.minFrameSize = minFrameSize;
this.maxFrameSize = maxFrameSize; this.maxFrameSize = maxFrameSize;
this.sampleRate = sampleRate; this.sampleRate = sampleRate;
this.sampleRateLookupKey = getSampleRateLookupKey(); this.sampleRateLookupKey = getSampleRateLookupKey(sampleRate);
this.channels = channels; this.channels = channels;
this.bitsPerSample = bitsPerSample; this.bitsPerSample = bitsPerSample;
this.bitsPerSampleLookupKey = getBitsPerSampleLookupKey(); this.bitsPerSampleLookupKey = getBitsPerSampleLookupKey(bitsPerSample);
this.totalSamples = totalSamples; this.totalSamples = totalSamples;
this.metadata = getMetadata(vorbisComments, pictureFrames); this.metadata = metadata;
} }
/** Returns the maximum size for a decoded frame from the FLAC stream. */ /** Returns the maximum size for a decoded frame from the FLAC stream. */
@ -193,12 +206,15 @@ public final class FlacStreamMetadata {
* *
* @param streamMarkerAndInfoBlock An array containing the FLAC stream marker followed by the * @param streamMarkerAndInfoBlock An array containing the FLAC stream marker followed by the
* stream info block. * stream info block.
* @param id3Metadata The ID3 metadata of the stream, or {@code null} if there is no such data.
* @return The extracted {@link Format}. * @return The extracted {@link Format}.
*/ */
public Format getFormat(byte[] streamMarkerAndInfoBlock) { public Format getFormat(byte[] streamMarkerAndInfoBlock, @Nullable Metadata id3Metadata) {
// Set the last metadata block flag, ignore the other blocks. // Set the last metadata block flag, ignore the other blocks.
streamMarkerAndInfoBlock[4] = (byte) 0x80; streamMarkerAndInfoBlock[4] = (byte) 0x80;
int maxInputSize = maxFrameSize > 0 ? maxFrameSize : Format.NO_VALUE; int maxInputSize = maxFrameSize > 0 ? maxFrameSize : Format.NO_VALUE;
Metadata metadataWithId3 = metadata.copyWithAppendedEntriesFrom(id3Metadata);
return Format.createAudioSampleFormat( return Format.createAudioSampleFormat(
/* id= */ null, /* id= */ null,
MimeTypes.AUDIO_FLAC, MimeTypes.AUDIO_FLAC,
@ -207,13 +223,55 @@ public final class FlacStreamMetadata {
maxInputSize, maxInputSize,
channels, channels,
sampleRate, sampleRate,
Collections.singletonList(streamMarkerAndInfoBlock), /* pcmEncoding= */ Format.NO_VALUE,
/* encoderDelay= */ 0,
/* encoderPadding= */ 0,
/* initializationData= */ Collections.singletonList(streamMarkerAndInfoBlock),
/* drmInitData= */ null, /* drmInitData= */ null,
/* selectionFlags= */ 0, /* selectionFlags= */ 0,
/* language= */ null); /* language= */ null,
metadataWithId3);
} }
private int getSampleRateLookupKey() { /** Returns a copy of the content metadata with entries from {@code other} appended. */
public Metadata getMetadataCopyWithAppendedEntriesFrom(@Nullable Metadata other) {
return metadata.copyWithAppendedEntriesFrom(other);
}
/** Returns a copy of {@code this} with the given Vorbis comments added to the metadata. */
public FlacStreamMetadata copyWithVorbisComments(List<String> vorbisComments) {
Metadata appendedMetadata =
metadata.copyWithAppendedEntriesFrom(
buildMetadata(vorbisComments, Collections.emptyList()));
return new FlacStreamMetadata(
minBlockSizeSamples,
maxBlockSizeSamples,
minFrameSize,
maxFrameSize,
sampleRate,
channels,
bitsPerSample,
totalSamples,
appendedMetadata);
}
/** Returns a copy of {@code this} with the given picture frames added to the metadata. */
public FlacStreamMetadata copyWithPictureFrames(List<PictureFrame> pictureFrames) {
Metadata appendedMetadata =
metadata.copyWithAppendedEntriesFrom(buildMetadata(Collections.emptyList(), pictureFrames));
return new FlacStreamMetadata(
minBlockSizeSamples,
maxBlockSizeSamples,
minFrameSize,
maxFrameSize,
sampleRate,
channels,
bitsPerSample,
totalSamples,
appendedMetadata);
}
private static int getSampleRateLookupKey(int sampleRate) {
switch (sampleRate) { switch (sampleRate) {
case 88200: case 88200:
return 1; return 1;
@ -242,7 +300,7 @@ public final class FlacStreamMetadata {
} }
} }
private int getBitsPerSampleLookupKey() { private static int getBitsPerSampleLookupKey(int bitsPerSample) {
switch (bitsPerSample) { switch (bitsPerSample) {
case 8: case 8:
return 1; return 1;
@ -259,11 +317,10 @@ public final class FlacStreamMetadata {
} }
} }
@Nullable private static Metadata buildMetadata(
private static Metadata getMetadata(
List<String> vorbisComments, List<PictureFrame> pictureFrames) { List<String> vorbisComments, List<PictureFrame> pictureFrames) {
if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) { if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) {
return null; return new Metadata();
} }
ArrayList<Metadata.Entry> metadataEntries = new ArrayList<>(); ArrayList<Metadata.Entry> metadataEntries = new ArrayList<>();
@ -271,7 +328,7 @@ public final class FlacStreamMetadata {
String vorbisComment = vorbisComments.get(i); String vorbisComment = vorbisComments.get(i);
String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR); String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR);
if (keyAndValue.length != 2) { if (keyAndValue.length != 2) {
Log.w(TAG, "Failed to parse vorbis comment: " + vorbisComment); Log.w(TAG, "Failed to parse Vorbis comment: " + vorbisComment);
} else { } else {
VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]); VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]);
metadataEntries.add(entry); metadataEntries.add(entry);
@ -279,6 +336,6 @@ public final class FlacStreamMetadata {
} }
metadataEntries.addAll(pictureFrames); metadataEntries.addAll(pictureFrames);
return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries); return metadataEntries.isEmpty() ? new Metadata() : new Metadata(metadataEntries);
} }
} }

Binary file not shown.

View File

@ -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

View File

@ -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

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.extractor.ogg; package com.google.android.exoplayer2.extractor;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;

View File

@ -13,16 +13,19 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.extractor.ogg; package com.google.android.exoplayer2.extractor;
import static com.google.android.exoplayer2.extractor.ogg.VorbisUtil.iLog; import static com.google.android.exoplayer2.extractor.VorbisUtil.iLog;
import static com.google.android.exoplayer2.extractor.ogg.VorbisUtil.verifyVorbisHeaderCapturePattern; import static com.google.android.exoplayer2.extractor.VorbisUtil.verifyVorbisHeaderCapturePattern;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -45,7 +48,9 @@ public final class VorbisUtilTest {
@Test @Test
public void testReadIdHeader() throws Exception { public void testReadIdHeader() throws Exception {
byte[] data = OggTestData.getIdentificationHeaderData(); byte[] data =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), "binary/vorbis/id_header");
ParsableByteArray headerData = new ParsableByteArray(data, data.length); ParsableByteArray headerData = new ParsableByteArray(data, data.length);
VorbisUtil.VorbisIdHeader vorbisIdHeader = VorbisUtil.VorbisIdHeader vorbisIdHeader =
VorbisUtil.readVorbisIdentificationHeader(headerData); VorbisUtil.readVorbisIdentificationHeader(headerData);
@ -63,8 +68,10 @@ public final class VorbisUtilTest {
} }
@Test @Test
public void testReadCommentHeader() throws ParserException { public void testReadCommentHeader() throws IOException {
byte[] data = OggTestData.getCommentHeaderDataUTF8(); byte[] data =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), "binary/vorbis/comment_header");
ParsableByteArray headerData = new ParsableByteArray(data, data.length); ParsableByteArray headerData = new ParsableByteArray(data, data.length);
VorbisUtil.CommentHeader commentHeader = VorbisUtil.readVorbisCommentHeader(headerData); VorbisUtil.CommentHeader commentHeader = VorbisUtil.readVorbisCommentHeader(headerData);
@ -76,8 +83,10 @@ public final class VorbisUtilTest {
} }
@Test @Test
public void testReadVorbisModes() throws ParserException { public void testReadVorbisModes() throws IOException {
byte[] data = OggTestData.getSetupHeaderData(); byte[] data =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), "binary/vorbis/setup_header");
ParsableByteArray headerData = new ParsableByteArray(data, data.length); ParsableByteArray headerData = new ParsableByteArray(data, data.length);
VorbisUtil.Mode[] modes = VorbisUtil.readVorbisModes(headerData, 2); VorbisUtil.Mode[] modes = VorbisUtil.readVorbisModes(headerData, 2);

View File

@ -30,10 +30,30 @@ public class FlacExtractorTest {
} }
@Test @Test
public void testSampleWithId3() throws Exception { public void testSampleWithId3HeaderAndId3Enabled() throws Exception {
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_with_id3.flac"); ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_with_id3.flac");
} }
@Test
public void testSampleWithId3HeaderAndId3Disabled() throws Exception {
// The same file is used for testing the extractor with and without ID3 enabled as the test does
// not check the metadata outputted. It only checks that the file is parsed correctly in both
// cases.
ExtractorAsserts.assertBehavior(
() -> new FlacExtractor(FlacExtractor.FLAG_DISABLE_ID3_METADATA),
"flac/bear_with_id3.flac");
}
@Test
public void testSampleWithVorbisComments() throws Exception {
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_with_vorbis_comments.flac");
}
@Test
public void testSampleWithPicture() throws Exception {
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_with_picture.flac");
}
@Test @Test
public void testOneMetadataBlock() throws Exception { public void testOneMetadataBlock() throws Exception {
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_one_metadata_block.flac"); ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_one_metadata_block.flac");

View File

@ -19,11 +19,13 @@ import static com.google.android.exoplayer2.extractor.ogg.VorbisReader.readBits;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ogg.VorbisReader.VorbisSetup; import com.google.android.exoplayer2.extractor.ogg.VorbisReader.VorbisSetup;
import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
import org.junit.Test; import org.junit.Test;
@ -55,7 +57,11 @@ public final class VorbisReaderTest {
@Test @Test
public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException { public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException {
byte[] data = OggTestData.getVorbisHeaderPages(); // initial two pages of bytes which by spec contain the three Vorbis header packets:
// identification, comment and setup header.
byte[] data =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), "binary/ogg/vorbis_header_pages");
ExtractorInput input = new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) ExtractorInput input = new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true)
.setSimulateUnknownLength(true).setSimulatePartialReads(true).build(); .setSimulateUnknownLength(true).setSimulatePartialReads(true).build();

View File

@ -35,7 +35,18 @@ public final class FlacStreamMetadataTest {
commentsList.add("Artist=Singer"); commentsList.add("Artist=Singer");
Metadata metadata = Metadata metadata =
new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; new FlacStreamMetadata(
/* minBlockSizeSamples= */ 0,
/* maxBlockSizeSamples= */ 0,
/* minFrameSize= */ 0,
/* maxFrameSize= */ 0,
/* sampleRate= */ 0,
/* channels= */ 0,
/* bitsPerSample= */ 0,
/* totalSamples= */ 0,
commentsList,
/* pictureFrames= */ new ArrayList<>())
.getMetadataCopyWithAppendedEntriesFrom(/* other= */ null);
assertThat(metadata.length()).isEqualTo(2); assertThat(metadata.length()).isEqualTo(2);
VorbisComment commentFrame = (VorbisComment) metadata.get(0); VorbisComment commentFrame = (VorbisComment) metadata.get(0);
@ -51,9 +62,20 @@ public final class FlacStreamMetadataTest {
ArrayList<String> commentsList = new ArrayList<>(); ArrayList<String> commentsList = new ArrayList<>();
Metadata metadata = Metadata metadata =
new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; new FlacStreamMetadata(
/* minBlockSizeSamples= */ 0,
/* maxBlockSizeSamples= */ 0,
/* minFrameSize= */ 0,
/* maxFrameSize= */ 0,
/* sampleRate= */ 0,
/* channels= */ 0,
/* bitsPerSample= */ 0,
/* totalSamples= */ 0,
commentsList,
/* pictureFrames= */ new ArrayList<>())
.getMetadataCopyWithAppendedEntriesFrom(/* other= */ null);
assertThat(metadata).isNull(); assertThat(metadata.length()).isEqualTo(0);
} }
@Test @Test
@ -62,7 +84,18 @@ public final class FlacStreamMetadataTest {
commentsList.add("Title=So=ng"); commentsList.add("Title=So=ng");
Metadata metadata = Metadata metadata =
new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; new FlacStreamMetadata(
/* minBlockSizeSamples= */ 0,
/* maxBlockSizeSamples= */ 0,
/* minFrameSize= */ 0,
/* maxFrameSize= */ 0,
/* sampleRate= */ 0,
/* channels= */ 0,
/* bitsPerSample= */ 0,
/* totalSamples= */ 0,
commentsList,
/* pictureFrames= */ new ArrayList<>())
.getMetadataCopyWithAppendedEntriesFrom(/* other= */ null);
assertThat(metadata.length()).isEqualTo(1); assertThat(metadata.length()).isEqualTo(1);
VorbisComment commentFrame = (VorbisComment) metadata.get(0); VorbisComment commentFrame = (VorbisComment) metadata.get(0);
@ -77,7 +110,18 @@ public final class FlacStreamMetadataTest {
commentsList.add("Artist=Singer"); commentsList.add("Artist=Singer");
Metadata metadata = Metadata metadata =
new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; new FlacStreamMetadata(
/* minBlockSizeSamples= */ 0,
/* maxBlockSizeSamples= */ 0,
/* minFrameSize= */ 0,
/* maxFrameSize= */ 0,
/* sampleRate= */ 0,
/* channels= */ 0,
/* bitsPerSample= */ 0,
/* totalSamples= */ 0,
commentsList,
/* pictureFrames= */ new ArrayList<>())
.getMetadataCopyWithAppendedEntriesFrom(/* other= */ null);
assertThat(metadata.length()).isEqualTo(1); assertThat(metadata.length()).isEqualTo(1);
VorbisComment commentFrame = (VorbisComment) metadata.get(0); VorbisComment commentFrame = (VorbisComment) metadata.get(0);

View File

@ -152,6 +152,7 @@ public final class ExtractorAsserts {
assertOutput(factory.create(), file, data, context, false, false, false, false); assertOutput(factory.create(), file, data, context, false, false, false, false);
} }
// TODO: Assert format metadata [Internal ref: b/144771011].
/** /**
* Asserts that {@code extractor} consumes {@code sampleFile} successfully and its output equals * Asserts that {@code extractor} consumes {@code sampleFile} successfully and its output equals
* to a prerecorded output dump file with the name {@code sampleFile} + "{@value * to a prerecorded output dump file with the name {@code sampleFile} + "{@value