Merge pull request #6151 from ittiam-systems:bug-5527

PiperOrigin-RevId: 257668797
This commit is contained in:
Oliver Woodman 2019-07-14 16:24:00 +01:00
parent b6777e030e
commit bba0a27cb6
16 changed files with 423 additions and 87 deletions

View File

@ -3,6 +3,8 @@
### 2.10.4 ### ### 2.10.4 ###
* Offline: Add Scheduler implementation which uses WorkManager. * Offline: Add Scheduler implementation which uses WorkManager.
* Flac extension: Parse `VORBIS_COMMENT` metadata
([#5527](https://github.com/google/ExoPlayer/issues/5527)).
### 2.10.3 ### ### 2.10.3 ###

View File

@ -9,6 +9,6 @@
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni { -keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
*; *;
} }
-keep class com.google.android.exoplayer2.util.FlacStreamInfo { -keep class com.google.android.exoplayer2.util.FlacStreamMetadata {
*; *;
} }

View File

@ -52,7 +52,10 @@ public final class FlacBinarySearchSeekerTest {
FlacBinarySearchSeeker seeker = FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker( new FlacBinarySearchSeeker(
decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni); decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0,
data.length,
decoderJni);
SeekMap seekMap = seeker.getSeekMap(); SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull(); assertThat(seekMap).isNotNull();
@ -70,7 +73,10 @@ public final class FlacBinarySearchSeekerTest {
decoderJni.setData(input); decoderJni.setData(input);
FlacBinarySearchSeeker seeker = FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker( new FlacBinarySearchSeeker(
decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni); decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0,
data.length,
decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000); seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.isSeeking()).isTrue(); assertThat(seeker.isSeeking()).isTrue();

View File

@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -34,20 +34,20 @@ import java.nio.ByteBuffer;
private final FlacDecoderJni decoderJni; private final FlacDecoderJni decoderJni;
public FlacBinarySearchSeeker( public FlacBinarySearchSeeker(
FlacStreamInfo streamInfo, FlacStreamMetadata streamMetadata,
long firstFramePosition, long firstFramePosition,
long inputLength, long inputLength,
FlacDecoderJni decoderJni) { FlacDecoderJni decoderJni) {
super( super(
new FlacSeekTimestampConverter(streamInfo), new FlacSeekTimestampConverter(streamMetadata),
new FlacTimestampSeeker(decoderJni), new FlacTimestampSeeker(decoderJni),
streamInfo.durationUs(), streamMetadata.durationUs(),
/* floorTimePosition= */ 0, /* floorTimePosition= */ 0,
/* ceilingTimePosition= */ streamInfo.totalSamples, /* ceilingTimePosition= */ streamMetadata.totalSamples,
/* floorBytePosition= */ firstFramePosition, /* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength, /* ceilingBytePosition= */ inputLength,
/* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(), /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize)); /* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni); this.decoderJni = Assertions.checkNotNull(decoderJni);
} }
@ -112,15 +112,15 @@ import java.nio.ByteBuffer;
* the timestamp for a stream seek time position. * the timestamp for a stream seek time position.
*/ */
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter { private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
private final FlacStreamInfo streamInfo; private final FlacStreamMetadata streamMetadata;
public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) { public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) {
this.streamInfo = streamInfo; this.streamMetadata = streamMetadata;
} }
@Override @Override
public long timeUsToTargetTime(long timeUs) { public long timeUsToTargetTime(long timeUs) {
return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs); return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs);
} }
} }
} }

View File

@ -21,7 +21,7 @@ import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
@ -58,9 +58,9 @@ import java.util.List;
} }
decoderJni = new FlacDecoderJni(); decoderJni = new FlacDecoderJni();
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
FlacStreamInfo streamInfo; FlacStreamMetadata streamMetadata;
try { try {
streamInfo = decoderJni.decodeStreamInfo(); streamMetadata = decoderJni.decodeStreamMetadata();
} catch (ParserException e) { } catch (ParserException e) {
throw new FlacDecoderException("Failed to decode StreamInfo", e); throw new FlacDecoderException("Failed to decode StreamInfo", e);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
@ -69,9 +69,9 @@ import java.util.List;
} }
int initialInputBufferSize = int initialInputBufferSize =
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize; maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
setInitialInputBufferSize(initialInputBufferSize); setInitialInputBufferSize(initialInputBufferSize);
maxOutputBufferSize = streamInfo.maxDecodedFrameSize(); maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();
} }
@Override @Override

View File

@ -19,7 +19,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -142,13 +142,13 @@ import java.nio.ByteBuffer;
return byteCount; return byteCount;
} }
/** Decodes and consumes the StreamInfo section from the FLAC stream. */ /** Decodes and consumes the metadata from the FLAC stream. */
public FlacStreamInfo decodeStreamInfo() throws IOException, InterruptedException { public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException {
FlacStreamInfo streamInfo = flacDecodeMetadata(nativeDecoderContext); FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext);
if (streamInfo == null) { if (streamMetadata == null) {
throw new ParserException("Failed to decode StreamInfo"); throw new ParserException("Failed to decode stream metadata");
} }
return streamInfo; return streamMetadata;
} }
/** /**
@ -266,7 +266,7 @@ import java.nio.ByteBuffer;
private native long flacInit(); private native long flacInit();
private native FlacStreamInfo flacDecodeMetadata(long context) private native FlacStreamMetadata flacDecodeMetadata(long context)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)

View File

@ -34,7 +34,7 @@ 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.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamInfo; 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;
import java.io.IOException; import java.io.IOException;
@ -86,8 +86,8 @@ public final class FlacExtractor implements Extractor {
private @MonotonicNonNull ExtractorOutput extractorOutput; private @MonotonicNonNull ExtractorOutput extractorOutput;
private @MonotonicNonNull TrackOutput trackOutput; private @MonotonicNonNull TrackOutput trackOutput;
private boolean streamInfoDecoded; private boolean streamMetadataDecoded;
private @MonotonicNonNull FlacStreamInfo streamInfo; private @MonotonicNonNull FlacStreamMetadata streamMetadata;
private @MonotonicNonNull OutputFrameHolder outputFrameHolder; private @MonotonicNonNull OutputFrameHolder outputFrameHolder;
@Nullable private Metadata id3Metadata; @Nullable private Metadata id3Metadata;
@ -138,7 +138,7 @@ public final class FlacExtractor implements Extractor {
FlacDecoderJni decoderJni = initDecoderJni(input); FlacDecoderJni decoderJni = initDecoderJni(input);
try { try {
decodeStreamInfo(input); decodeStreamMetadata(input);
if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput); return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput);
@ -166,7 +166,7 @@ public final class FlacExtractor implements Extractor {
@Override @Override
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
if (position == 0) { if (position == 0) {
streamInfoDecoded = false; streamMetadataDecoded = false;
} }
if (decoderJni != null) { if (decoderJni != null) {
decoderJni.reset(position); decoderJni.reset(position);
@ -207,29 +207,33 @@ public final class FlacExtractor implements Extractor {
} }
@RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized. @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
@EnsuresNonNull({"streamInfo", "outputFrameHolder"}) // Ensures StreamInfo decoded. @EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
@SuppressWarnings({"contracts.postcondition.not.satisfied"}) @SuppressWarnings({"contracts.postcondition.not.satisfied"})
private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException { private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException {
if (streamInfoDecoded) { if (streamMetadataDecoded) {
return; return;
} }
FlacStreamInfo streamInfo; FlacStreamMetadata streamMetadata;
try { try {
streamInfo = decoderJni.decodeStreamInfo(); streamMetadata = decoderJni.decodeStreamMetadata();
} catch (IOException e) { } catch (IOException e) {
decoderJni.reset(/* newPosition= */ 0); decoderJni.reset(/* newPosition= */ 0);
input.setRetryPosition(/* position= */ 0, e); input.setRetryPosition(/* position= */ 0, e);
throw e; throw e;
} }
streamInfoDecoded = true; streamMetadataDecoded = true;
if (this.streamInfo == null) { if (this.streamMetadata == null) {
this.streamInfo = streamInfo; this.streamMetadata = streamMetadata;
binarySearchSeeker = binarySearchSeeker =
outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput);
outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput); Metadata metadata = id3MetadataDisabled ? null : id3Metadata;
outputBuffer.reset(streamInfo.maxDecodedFrameSize()); if (streamMetadata.vorbisComments != null) {
metadata = streamMetadata.vorbisComments.copyWithAppendedEntriesFrom(metadata);
}
outputFormat(streamMetadata, metadata, trackOutput);
outputBuffer.reset(streamMetadata.maxDecodedFrameSize());
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
} }
} }
@ -269,38 +273,38 @@ public final class FlacExtractor implements Extractor {
@Nullable @Nullable
private static FlacBinarySearchSeeker outputSeekMap( private static FlacBinarySearchSeeker outputSeekMap(
FlacDecoderJni decoderJni, FlacDecoderJni decoderJni,
FlacStreamInfo streamInfo, FlacStreamMetadata streamMetadata,
long streamLength, long streamLength,
ExtractorOutput output) { ExtractorOutput output) {
boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1;
FlacBinarySearchSeeker binarySearchSeeker = null; FlacBinarySearchSeeker binarySearchSeeker = null;
SeekMap seekMap; SeekMap seekMap;
if (hasSeekTable) { if (hasSeekTable) {
seekMap = new FlacSeekMap(streamInfo.durationUs(), decoderJni); seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);
} else if (streamLength != C.LENGTH_UNSET) { } else if (streamLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition(); long firstFramePosition = decoderJni.getDecodePosition();
binarySearchSeeker = binarySearchSeeker =
new FlacBinarySearchSeeker(streamInfo, firstFramePosition, streamLength, decoderJni); new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni);
seekMap = binarySearchSeeker.getSeekMap(); seekMap = binarySearchSeeker.getSeekMap();
} else { } else {
seekMap = new SeekMap.Unseekable(streamInfo.durationUs()); seekMap = new SeekMap.Unseekable(streamMetadata.durationUs());
} }
output.seekMap(seekMap); output.seekMap(seekMap);
return binarySearchSeeker; return binarySearchSeeker;
} }
private static void outputFormat( private static void outputFormat(
FlacStreamInfo streamInfo, @Nullable Metadata metadata, TrackOutput output) { FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
Format mediaFormat = Format mediaFormat =
Format.createAudioSampleFormat( Format.createAudioSampleFormat(
/* id= */ null, /* id= */ null,
MimeTypes.AUDIO_RAW, MimeTypes.AUDIO_RAW,
/* codecs= */ null, /* codecs= */ null,
streamInfo.bitRate(), streamMetadata.bitRate(),
streamInfo.maxDecodedFrameSize(), streamMetadata.maxDecodedFrameSize(),
streamInfo.channels, streamMetadata.channels,
streamInfo.sampleRate, streamMetadata.sampleRate,
getPcmEncoding(streamInfo.bitsPerSample), getPcmEncoding(streamMetadata.bitsPerSample),
/* encoderDelay= */ 0, /* encoderDelay= */ 0,
/* encoderPadding= */ 0, /* encoderPadding= */ 0,
/* initializationData= */ null, /* initializationData= */ null,

View File

@ -14,9 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
#include <jni.h>
#include <android/log.h> #include <android/log.h>
#include <jni.h>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include "include/flac_parser.h" #include "include/flac_parser.h"
#define LOG_TAG "flac_jni" #define LOG_TAG "flac_jni"
@ -95,19 +98,40 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
return NULL; return NULL;
} }
jclass arrayListClass = env->FindClass("java/util/ArrayList");
jmethodID arrayListConstructor =
env->GetMethodID(arrayListClass, "<init>", "()V");
jobject commentList = env->NewObject(arrayListClass, arrayListConstructor);
if (context->parser->isVorbisCommentsValid()) {
jmethodID arrayListAddMethod =
env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
std::vector<std::string> vorbisComments =
context->parser->getVorbisComments();
for (std::vector<std::string>::const_iterator vorbisComment =
vorbisComments.begin();
vorbisComment != vorbisComments.end(); ++vorbisComment) {
jstring commentString = env->NewStringUTF((*vorbisComment).c_str());
env->CallBooleanMethod(commentList, arrayListAddMethod, commentString);
env->DeleteLocalRef(commentString);
}
}
const FLAC__StreamMetadata_StreamInfo &streamInfo = const FLAC__StreamMetadata_StreamInfo &streamInfo =
context->parser->getStreamInfo(); context->parser->getStreamInfo();
jclass cls = env->FindClass( jclass flacStreamMetadataClass = env->FindClass(
"com/google/android/exoplayer2/util/" "com/google/android/exoplayer2/util/"
"FlacStreamInfo"); "FlacStreamMetadata");
jmethodID constructor = env->GetMethodID(cls, "<init>", "(IIIIIIIJ)V"); jmethodID flacStreamMetadataConstructor = env->GetMethodID(
flacStreamMetadataClass, "<init>", "(IIIIIIIJLjava/util/List;)V");
return env->NewObject(cls, constructor, streamInfo.min_blocksize, return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
streamInfo.max_blocksize, streamInfo.min_framesize, streamInfo.min_blocksize, streamInfo.max_blocksize,
streamInfo.max_framesize, streamInfo.sample_rate, streamInfo.min_framesize, streamInfo.max_framesize,
streamInfo.channels, streamInfo.bits_per_sample, streamInfo.sample_rate, streamInfo.channels,
streamInfo.total_samples); streamInfo.bits_per_sample, streamInfo.total_samples,
commentList);
} }
DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {

View File

@ -172,6 +172,25 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) {
case FLAC__METADATA_TYPE_SEEKTABLE: case FLAC__METADATA_TYPE_SEEKTABLE:
mSeekTable = &metadata->data.seek_table; mSeekTable = &metadata->data.seek_table;
break; break;
case FLAC__METADATA_TYPE_VORBIS_COMMENT:
if (!mVorbisCommentsValid) {
FLAC__StreamMetadata_VorbisComment vorbisComment =
metadata->data.vorbis_comment;
for (FLAC__uint32 i = 0; i < vorbisComment.num_comments; ++i) {
FLAC__StreamMetadata_VorbisComment_Entry vorbisCommentEntry =
vorbisComment.comments[i];
if (vorbisCommentEntry.entry != NULL) {
std::string comment(
reinterpret_cast<char *>(vorbisCommentEntry.entry),
vorbisCommentEntry.length);
mVorbisComments.push_back(comment);
}
}
mVorbisCommentsValid = true;
} else {
ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT");
}
break;
default: default:
ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type);
break; break;
@ -233,6 +252,7 @@ FLACParser::FLACParser(DataSource *source)
mCurrentPos(0LL), mCurrentPos(0LL),
mEOF(false), mEOF(false),
mStreamInfoValid(false), mStreamInfoValid(false),
mVorbisCommentsValid(false),
mWriteRequested(false), mWriteRequested(false),
mWriteCompleted(false), mWriteCompleted(false),
mWriteBuffer(NULL), mWriteBuffer(NULL),
@ -266,6 +286,8 @@ bool FLACParser::init() {
FLAC__METADATA_TYPE_STREAMINFO); FLAC__METADATA_TYPE_STREAMINFO);
FLAC__stream_decoder_set_metadata_respond(mDecoder, FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_SEEKTABLE); FLAC__METADATA_TYPE_SEEKTABLE);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_VORBIS_COMMENT);
FLAC__StreamDecoderInitStatus initStatus; FLAC__StreamDecoderInitStatus initStatus;
initStatus = FLAC__stream_decoder_init_stream( initStatus = FLAC__stream_decoder_init_stream(
mDecoder, read_callback, seek_callback, tell_callback, length_callback, mDecoder, read_callback, seek_callback, tell_callback, length_callback,

View File

@ -19,6 +19,10 @@
#include <stdint.h> #include <stdint.h>
#include <cstdlib>
#include <string>
#include <vector>
// libFLAC parser // libFLAC parser
#include "FLAC/stream_decoder.h" #include "FLAC/stream_decoder.h"
@ -44,6 +48,10 @@ class FLACParser {
return mStreamInfo; return mStreamInfo;
} }
bool isVorbisCommentsValid() { return mVorbisCommentsValid; }
std::vector<std::string> getVorbisComments() { return mVorbisComments; }
int64_t getLastFrameTimestamp() const { int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
} }
@ -71,6 +79,8 @@ class FLACParser {
mEOF = false; mEOF = false;
if (newPosition == 0) { if (newPosition == 0) {
mStreamInfoValid = false; mStreamInfoValid = false;
mVorbisCommentsValid = false;
mVorbisComments.clear();
FLAC__stream_decoder_reset(mDecoder); FLAC__stream_decoder_reset(mDecoder);
} else { } else {
FLAC__stream_decoder_flush(mDecoder); FLAC__stream_decoder_flush(mDecoder);
@ -116,6 +126,10 @@ class FLACParser {
const FLAC__StreamMetadata_SeekTable *mSeekTable; const FLAC__StreamMetadata_SeekTable *mSeekTable;
uint64_t firstFrameOffset; uint64_t firstFrameOffset;
// cached when the VORBIS_COMMENT metadata is parsed by libFLAC
std::vector<std::string> mVorbisComments;
bool mVorbisCommentsValid;
// cached when a decoded PCM block is "written" by libFLAC parser // cached when a decoded PCM block is "written" by libFLAC parser
bool mWriteRequested; bool mWriteRequested;
bool mWriteCompleted; bool mWriteCompleted;

View File

@ -19,7 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
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.util.FlacStreamInfo; 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;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
@ -38,7 +38,7 @@ import java.util.List;
private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
private FlacStreamInfo streamInfo; private FlacStreamMetadata streamMetadata;
private FlacOggSeeker flacOggSeeker; private FlacOggSeeker flacOggSeeker;
public static boolean verifyBitstreamType(ParsableByteArray data) { public static boolean verifyBitstreamType(ParsableByteArray data) {
@ -50,7 +50,7 @@ import java.util.List;
protected void reset(boolean headerData) { protected void reset(boolean headerData) {
super.reset(headerData); super.reset(headerData);
if (headerData) { if (headerData) {
streamInfo = null; streamMetadata = null;
flacOggSeeker = null; flacOggSeeker = null;
} }
} }
@ -71,14 +71,24 @@ import java.util.List;
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
throws IOException, InterruptedException { throws IOException, InterruptedException {
byte[] data = packet.data; byte[] data = packet.data;
if (streamInfo == null) { if (streamMetadata == null) {
streamInfo = new FlacStreamInfo(data, 17); streamMetadata = new FlacStreamMetadata(data, 17);
byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks
List<byte[]> initializationData = Collections.singletonList(metadata); List<byte[]> initializationData = Collections.singletonList(metadata);
setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null, setupData.format =
Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate, Format.createAudioSampleFormat(
initializationData, null, 0, null); null,
MimeTypes.AUDIO_FLAC,
null,
Format.NO_VALUE,
streamMetadata.bitRate(),
streamMetadata.channels,
streamMetadata.sampleRate,
initializationData,
null,
0,
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);
@ -211,7 +221,7 @@ import java.util.List;
@Override @Override
public long getDurationUs() { public long getDurationUs() {
return streamInfo.durationUs(); return streamMetadata.durationUs();
} }
} }

View File

@ -0,0 +1,99 @@
/*
* 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.metadata.vorbis;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
/** A vorbis comment. */
public final class VorbisComment implements Metadata.Entry {
/** The key. */
public final String key;
/** The value. */
public final String value;
/**
* @param key The key.
* @param value The value.
*/
public VorbisComment(String key, String value) {
this.key = key;
this.value = value;
}
/* package */ VorbisComment(Parcel in) {
this.key = castNonNull(in.readString());
this.value = castNonNull(in.readString());
}
@Override
public String toString() {
return "VC: " + key + "=" + value;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
VorbisComment other = (VorbisComment) obj;
return key.equals(other.key) && value.equals(other.value);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + key.hashCode();
result = 31 * result + value.hashCode();
return result;
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(key);
dest.writeString(value);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<VorbisComment> CREATOR =
new Parcelable.Creator<VorbisComment>() {
@Override
public VorbisComment createFromParcel(Parcel in) {
return new VorbisComment(in);
}
@Override
public VorbisComment[] newArray(int size) {
return new VorbisComment[size];
}
};
}

View File

@ -15,12 +15,17 @@
*/ */
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment;
import java.util.ArrayList;
import java.util.List;
/** /** Holder for FLAC metadata. */
* Holder for FLAC stream info. public final class FlacStreamMetadata {
*/
public final class FlacStreamInfo { private static final String TAG = "FlacStreamMetadata";
public final int minBlockSize; public final int minBlockSize;
public final int maxBlockSize; public final int maxBlockSize;
@ -30,16 +35,19 @@ public final class FlacStreamInfo {
public final int channels; public final int channels;
public final int bitsPerSample; public final int bitsPerSample;
public final long totalSamples; public final long totalSamples;
@Nullable public final Metadata vorbisComments;
private static final String SEPARATOR = "=";
/** /**
* Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure. * Parses binary FLAC stream info metadata.
* *
* @param data An array holding FLAC stream info metadata structure * @param data An array containing binary FLAC stream info metadata.
* @param offset Offset of the structure in the array * @param offset The offset of the stream info metadata in {@code data}.
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format * @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a> * METADATA_BLOCK_STREAMINFO</a>
*/ */
public FlacStreamInfo(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.minBlockSize = scratch.readBits(16); this.minBlockSize = scratch.readBits(16);
@ -49,14 +57,11 @@ public final class FlacStreamInfo {
this.sampleRate = scratch.readBits(20); this.sampleRate = scratch.readBits(20);
this.channels = scratch.readBits(3) + 1; this.channels = scratch.readBits(3) + 1;
this.bitsPerSample = scratch.readBits(5) + 1; this.bitsPerSample = scratch.readBits(5) + 1;
this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL);
| (scratch.readBits(32) & 0xFFFFFFFFL); this.vorbisComments = null;
// Remaining 16 bytes is md5 value
} }
/** /**
* Constructs a FlacStreamInfo given the parameters.
*
* @param minBlockSize Minimum block size of the FLAC stream. * @param minBlockSize Minimum block size of the FLAC stream.
* @param maxBlockSize Maximum block size of the FLAC stream. * @param maxBlockSize Maximum block size of the FLAC stream.
* @param minFrameSize Minimum frame size of the FLAC stream. * @param minFrameSize Minimum frame size of the FLAC stream.
@ -65,10 +70,13 @@ public final class FlacStreamInfo {
* @param channels Number of channels 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 bitsPerSample Number of bits per sample of the FLAC stream.
* @param totalSamples Total samples of the FLAC stream. * @param totalSamples Total samples of the FLAC stream.
* @param vorbisComments Vorbis comments. Each entry must be in key=value form.
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format * @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a> * METADATA_BLOCK_STREAMINFO</a>
* @see <a href="https://xiph.org/flac/format.html#metadata_block_vorbis_comment">FLAC format
* METADATA_BLOCK_VORBIS_COMMENT</a>
*/ */
public FlacStreamInfo( public FlacStreamMetadata(
int minBlockSize, int minBlockSize,
int maxBlockSize, int maxBlockSize,
int minFrameSize, int minFrameSize,
@ -76,7 +84,8 @@ public final class FlacStreamInfo {
int sampleRate, int sampleRate,
int channels, int channels,
int bitsPerSample, int bitsPerSample,
long totalSamples) { long totalSamples,
List<String> vorbisComments) {
this.minBlockSize = minBlockSize; this.minBlockSize = minBlockSize;
this.maxBlockSize = maxBlockSize; this.maxBlockSize = maxBlockSize;
this.minFrameSize = minFrameSize; this.minFrameSize = minFrameSize;
@ -85,6 +94,7 @@ public final class FlacStreamInfo {
this.channels = channels; this.channels = channels;
this.bitsPerSample = bitsPerSample; this.bitsPerSample = bitsPerSample;
this.totalSamples = totalSamples; this.totalSamples = totalSamples;
this.vorbisComments = parseVorbisComments(vorbisComments);
} }
/** Returns the maximum size for a decoded frame from the FLAC stream. */ /** Returns the maximum size for a decoded frame from the FLAC stream. */
@ -126,4 +136,24 @@ public final class FlacStreamInfo {
} }
return approxBytesPerFrame; return approxBytesPerFrame;
} }
@Nullable
private static Metadata parseVorbisComments(@Nullable List<String> vorbisComments) {
if (vorbisComments == null || vorbisComments.isEmpty()) {
return null;
}
ArrayList<VorbisComment> commentFrames = new ArrayList<>();
for (String vorbisComment : vorbisComments) {
String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR);
if (keyAndValue.length != 2) {
Log.w(TAG, "Failed to parse vorbis comment: " + vorbisComment);
} else {
VorbisComment commentFrame = new VorbisComment(keyAndValue[0], keyAndValue[1]);
commentFrames.add(commentFrame);
}
}
return commentFrames.isEmpty() ? null : new Metadata(commentFrames);
}
} }

View File

@ -0,0 +1,42 @@
/*
* 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.metadata.vorbis;
import static com.google.common.truth.Truth.assertThat;
import android.os.Parcel;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Test for {@link VorbisComment}. */
@RunWith(AndroidJUnit4.class)
public final class VorbisCommentTest {
@Test
public void testParcelable() {
VorbisComment vorbisCommentFrameToParcel = new VorbisComment("key", "value");
Parcel parcel = Parcel.obtain();
vorbisCommentFrameToParcel.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
VorbisComment vorbisCommentFrameFromParcel = VorbisComment.CREATOR.createFromParcel(parcel);
assertThat(vorbisCommentFrameFromParcel).isEqualTo(vorbisCommentFrameToParcel);
parcel.recycle();
}
}

View File

@ -28,7 +28,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** Unit test for <code>ColorParser</code>. */ /** Unit test for {@link ColorParser}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class ColorParserTest { public final class ColorParserTest {

View File

@ -0,0 +1,83 @@
/*
* 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 static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment;
import java.util.ArrayList;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link FlacStreamMetadata}. */
@RunWith(AndroidJUnit4.class)
public final class FlacStreamMetadataTest {
@Test
public void parseVorbisComments() {
ArrayList<String> commentsList = new ArrayList<>();
commentsList.add("Title=Song");
commentsList.add("Artist=Singer");
Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments;
assertThat(metadata.length()).isEqualTo(2);
VorbisComment commentFrame = (VorbisComment) metadata.get(0);
assertThat(commentFrame.key).isEqualTo("Title");
assertThat(commentFrame.value).isEqualTo("Song");
commentFrame = (VorbisComment) metadata.get(1);
assertThat(commentFrame.key).isEqualTo("Artist");
assertThat(commentFrame.value).isEqualTo("Singer");
}
@Test
public void parseEmptyVorbisComments() {
ArrayList<String> commentsList = new ArrayList<>();
Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments;
assertThat(metadata).isNull();
}
@Test
public void parseVorbisCommentWithEqualsInValue() {
ArrayList<String> commentsList = new ArrayList<>();
commentsList.add("Title=So=ng");
Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments;
assertThat(metadata.length()).isEqualTo(1);
VorbisComment commentFrame = (VorbisComment) metadata.get(0);
assertThat(commentFrame.key).isEqualTo("Title");
assertThat(commentFrame.value).isEqualTo("So=ng");
}
@Test
public void parseInvalidVorbisComment() {
ArrayList<String> commentsList = new ArrayList<>();
commentsList.add("TitleSong");
commentsList.add("Artist=Singer");
Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments;
assertThat(metadata.length()).isEqualTo(1);
VorbisComment commentFrame = (VorbisComment) metadata.get(0);
assertThat(commentFrame.key).isEqualTo("Artist");
assertThat(commentFrame.value).isEqualTo("Singer");
}
}