mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Expose metadata in FLAC extractor
PiperOrigin-RevId: 281538423
This commit is contained in:
parent
f6853e4751
commit
b18650fdcf
@ -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.
|
||||||
|
@ -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));
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
@ -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() {}
|
||||||
|
}
|
@ -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;
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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() {}
|
|
||||||
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
library/core/src/test/assets/binary/ogg/vorbis_header_pages
Normal file
BIN
library/core/src/test/assets/binary/ogg/vorbis_header_pages
Normal file
Binary file not shown.
BIN
library/core/src/test/assets/binary/vorbis/comment_header
Normal file
BIN
library/core/src/test/assets/binary/vorbis/comment_header
Normal file
Binary file not shown.
BIN
library/core/src/test/assets/binary/vorbis/id_header
Normal file
BIN
library/core/src/test/assets/binary/vorbis/id_header
Normal file
Binary file not shown.
BIN
library/core/src/test/assets/binary/vorbis/setup_header
Normal file
BIN
library/core/src/test/assets/binary/vorbis/setup_header
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
library/core/src/test/assets/flac/bear_with_picture.flac
Normal file
BIN
library/core/src/test/assets/flac/bear_with_picture.flac
Normal file
Binary file not shown.
163
library/core/src/test/assets/flac/bear_with_picture.flac.0.dump
Normal file
163
library/core/src/test/assets/flac/bear_with_picture.flac.0.dump
Normal 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
|
BIN
library/core/src/test/assets/flac/bear_with_vorbis_comments.flac
Normal file
BIN
library/core/src/test/assets/flac/bear_with_vorbis_comments.flac
Normal file
Binary file not shown.
@ -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
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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");
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user