From 77e1e4cc1e3a90219bc1f1e128f4d74c7f7a9700 Mon Sep 17 00:00:00 2001 From: "Venkatarama NG. Avadhani" Date: Tue, 9 Jul 2019 12:17:54 +0530 Subject: [PATCH] Add vorbis comments support to flac extractor Decode and add vorbis comments from the flac file to metadata. #5527 --- .../exoplayer2/ext/flac/FlacDecoderJni.java | 10 ++ .../exoplayer2/ext/flac/FlacExtractor.java | 23 +++- extensions/flac/src/main/jni/flac_jni.cc | 26 +++++ extensions/flac/src/main/jni/flac_parser.cc | 28 +++++ .../flac/src/main/jni/include/flac_parser.h | 14 +++ .../metadata/vorbis/VorbisCommentDecoder.java | 59 ++++++++++ .../metadata/vorbis/VorbisCommentFrame.java | 102 ++++++++++++++++++ .../vorbis/VorbisCommentDecoderTest.java | 90 ++++++++++++++++ .../vorbis/VorbisCommentFrameTest.java | 43 ++++++++ 9 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrameTest.java diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index 32ef22dab0..448e2a1b05 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; /** * JNI wrapper for the libflac Flac decoder. @@ -151,6 +152,12 @@ import java.nio.ByteBuffer; return streamInfo; } + /** Decodes and consumes the Vorbis Comment section from the FLAC stream. */ + @Nullable + public ArrayList decodeVorbisComment() throws IOException, InterruptedException { + return flacDecodeVorbisComment(nativeDecoderContext); + } + /** * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO * error occurs, resets the stream and input to the given {@code retryPosition}. @@ -269,6 +276,9 @@ import java.nio.ByteBuffer; private native FlacStreamInfo flacDecodeMetadata(long context) throws IOException, InterruptedException; + private native ArrayList flacDecodeVorbisComment(long context) + throws IOException, InterruptedException; + private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) throws IOException, InterruptedException; diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 082068f34d..307cdfa8c8 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import com.google.android.exoplayer2.metadata.vorbis.VorbisCommentDecoder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; @@ -42,6 +43,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Arrays; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -91,6 +93,7 @@ public final class FlacExtractor implements Extractor { private @MonotonicNonNull OutputFrameHolder outputFrameHolder; @Nullable private Metadata id3Metadata; + @Nullable private Metadata vorbisMetadata; @Nullable private FlacBinarySearchSeeker binarySearchSeeker; /** Constructs an instance with flags = 0. */ @@ -224,11 +227,16 @@ public final class FlacExtractor implements Extractor { } streamInfoDecoded = true; + vorbisMetadata = decodeVorbisComment(input); if (this.streamInfo == null) { this.streamInfo = streamInfo; binarySearchSeeker = outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput); - outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput); + Metadata metadata = id3MetadataDisabled ? null : id3Metadata; + if (vorbisMetadata != null) { + metadata = vorbisMetadata.copyWithAppendedEntriesFrom(metadata); + } + outputFormat(streamInfo, metadata, trackOutput); outputBuffer.reset(streamInfo.maxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); } @@ -262,6 +270,19 @@ public final class FlacExtractor implements Extractor { return Arrays.equals(header, FLAC_SIGNATURE); } + @Nullable + private Metadata decodeVorbisComment(ExtractorInput input) + throws InterruptedException, IOException { + try { + ArrayList vorbisCommentList = decoderJni.decodeVorbisComment(); + return new VorbisCommentDecoder().decodeVorbisComments(vorbisCommentList); + } catch (IOException e) { + decoderJni.reset(0); + input.setRetryPosition(0, e); + throw e; + } + } + /** * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to * handle seeks. diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 298719d48d..0971ba5883 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -110,6 +110,32 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { streamInfo.total_samples); } +DECODER_FUNC(jobject, flacDecodeVorbisComment, jlong jContext) { + Context *context = reinterpret_cast(jContext); + context->source->setFlacDecoderJni(env, thiz); + + VorbisComment vorbisComment = context->parser->getVorbisComment(); + + if (vorbisComment.numComments == 0) { + return NULL; + } else { + jclass java_util_ArrayList = env->FindClass("java/util/ArrayList"); + + jmethodID java_util_ArrayList_ = env->GetMethodID(java_util_ArrayList, "", "(I)V"); + jmethodID java_util_ArrayList_add = env->GetMethodID(java_util_ArrayList, "add", + "(Ljava/lang/Object;)Z"); + + jobject result = env->NewObject(java_util_ArrayList, java_util_ArrayList_, + vorbisComment.numComments); + for (FLAC__uint32 i = 0; i < vorbisComment.numComments; ++i) { + jstring element = env->NewStringUTF(vorbisComment.metadataArray[i]); + env->CallBooleanMethod(result, java_util_ArrayList_add, element); + env->DeleteLocalRef(element); + } + return result; + } +} + DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { Context *context = reinterpret_cast(jContext); context->source->setFlacDecoderJni(env, thiz); diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 83d3367415..06c98302fd 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -172,6 +172,30 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { case FLAC__METADATA_TYPE_SEEKTABLE: mSeekTable = &metadata->data.seek_table; break; + case FLAC__METADATA_TYPE_VORBIS_COMMENT: + if (!mVorbisCommentValid) { + FLAC__uint32 count = 0; + const FLAC__StreamMetadata_VorbisComment *vc = + &metadata->data.vorbis_comment; + mVorbisCommentValid = true; + mVorbisComment.metadataArray = + (char **) malloc(vc->num_comments * sizeof(char *)); + for (FLAC__uint32 i = 0; i < vc->num_comments; ++i) { + FLAC__StreamMetadata_VorbisComment_Entry *vce = &vc->comments[i]; + if (vce->entry != NULL) { + mVorbisComment.metadataArray[count] = + (char *) malloc((vce->length + 1) * sizeof(char)); + memcpy(mVorbisComment.metadataArray[count], vce->entry, + vce->length); + mVorbisComment.metadataArray[count][vce->length] = '\0'; + count++; + } + } + mVorbisComment.numComments = count; + } else { + ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT"); + } + break; default: ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); break; @@ -233,6 +257,7 @@ FLACParser::FLACParser(DataSource *source) mCurrentPos(0LL), mEOF(false), mStreamInfoValid(false), + mVorbisCommentValid(false), mWriteRequested(false), mWriteCompleted(false), mWriteBuffer(NULL), @@ -240,6 +265,7 @@ FLACParser::FLACParser(DataSource *source) ALOGV("FLACParser::FLACParser"); memset(&mStreamInfo, 0, sizeof(mStreamInfo)); memset(&mWriteHeader, 0, sizeof(mWriteHeader)); + memset(&mVorbisComment, 0, sizeof(mVorbisComment)); } FLACParser::~FLACParser() { @@ -266,6 +292,8 @@ bool FLACParser::init() { FLAC__METADATA_TYPE_STREAMINFO); FLAC__stream_decoder_set_metadata_respond(mDecoder, FLAC__METADATA_TYPE_SEEKTABLE); + FLAC__stream_decoder_set_metadata_respond(mDecoder, + FLAC__METADATA_TYPE_VORBIS_COMMENT); FLAC__StreamDecoderInitStatus initStatus; initStatus = FLAC__stream_decoder_init_stream( mDecoder, read_callback, seek_callback, tell_callback, length_callback, diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index cea7fbe33b..aec07d673e 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -26,6 +26,11 @@ typedef int status_t; +typedef struct VorbisComment_ { + int numComments; + char **metadataArray; +} VorbisComment; + class FLACParser { public: FLACParser(DataSource *source); @@ -71,6 +76,7 @@ class FLACParser { mEOF = false; if (newPosition == 0) { mStreamInfoValid = false; + mVorbisCommentValid = false; FLAC__stream_decoder_reset(mDecoder); } else { FLAC__stream_decoder_flush(mDecoder); @@ -96,6 +102,10 @@ class FLACParser { FLAC__STREAM_DECODER_END_OF_STREAM; } + VorbisComment getVorbisComment() { + return mVorbisComment; + } + private: DataSource *mDataSource; @@ -116,6 +126,8 @@ class FLACParser { const FLAC__StreamMetadata_SeekTable *mSeekTable; uint64_t firstFrameOffset; + bool mVorbisCommentValid; + // cached when a decoded PCM block is "written" by libFLAC parser bool mWriteRequested; bool mWriteCompleted; @@ -129,6 +141,8 @@ class FLACParser { FLACParser(const FLACParser &); FLACParser &operator=(const FLACParser &); + VorbisComment mVorbisComment; + // FLAC parser callbacks as C++ instance methods FLAC__StreamDecoderReadStatus readCallback(FLAC__byte buffer[], size_t *bytes); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java new file mode 100644 index 0000000000..a212532685 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoder.java @@ -0,0 +1,59 @@ +/* + * 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 androidx.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; +import java.util.ArrayList; + +/** Decodes vorbis comments */ +public class VorbisCommentDecoder { + + private static final String SEPARATOR = "="; + + /** + * Decodes an {@link ArrayList} of vorbis comments. + * + * @param metadataStringList An {@link ArrayList} containing vorbis comments as {@link String} + * @return A {@link Metadata} structure with the vorbis comments as its entries. + */ + public Metadata decodeVorbisComments(@Nullable ArrayList metadataStringList) { + if (metadataStringList == null || metadataStringList.size() == 0) { + return null; + } + + ArrayList vorbisCommentFrames = new ArrayList<>(); + VorbisCommentFrame vorbisCommentFrame; + + for (String commentEntry : metadataStringList) { + String[] keyValue; + + keyValue = commentEntry.split(SEPARATOR); + if (keyValue.length != 2) { + /* Could not parse this comment, no key value pair found */ + continue; + } + vorbisCommentFrame = new VorbisCommentFrame(keyValue[0], keyValue[1]); + vorbisCommentFrames.add(vorbisCommentFrame); + } + + if (vorbisCommentFrames.size() > 0) { + return new Metadata(vorbisCommentFrames); + } else { + return null; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java new file mode 100644 index 0000000000..2deb5b1127 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrame.java @@ -0,0 +1,102 @@ +/* + * 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; + +/** Base class for Vorbis Comment Frames. */ +public class VorbisCommentFrame implements Metadata.Entry { + + /** The frame key and value */ + public final String key; + + public final String value; + + /** + * @param key The key + * @param value Value corresponding to the key + */ + public VorbisCommentFrame(String key, String value) { + this.key = key; + this.value = value; + } + + /* package */ VorbisCommentFrame(Parcel in) { + this.key = castNonNull(in.readString()); + this.value = castNonNull(in.readString()); + } + + @Override + public String toString() { + return key; + } + + @Override + public int describeContents() { + return 0; + } + + // Parcelable implementation. + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(key); + dest.writeString(value); + } + + @Override + public boolean equals(@Nullable Object obj) { + if ((obj != null) && (obj.getClass() == this.getClass())) { + if (this == obj) { + return true; + } else { + VorbisCommentFrame compareFrame = (VorbisCommentFrame) obj; + if (this.key.equals(compareFrame.key) && this.value.equals(compareFrame.value)) { + return true; + } + } + } + return false; + } + + @Override + public int hashCode() { + int result = 17; + + result = 31 * result + key.hashCode(); + result = 31 * result + value.hashCode(); + + return result; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public VorbisCommentFrame createFromParcel(Parcel in) { + return new VorbisCommentFrame(in); + } + + @Override + public VorbisCommentFrame[] newArray(int size) { + return new VorbisCommentFrame[size]; + } + }; +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java new file mode 100644 index 0000000000..e2c2bcf021 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentDecoderTest.java @@ -0,0 +1,90 @@ +/* + * 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 androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.metadata.Metadata; +import java.util.ArrayList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link VorbisCommentDecoder}. */ +@RunWith(AndroidJUnit4.class) +public final class VorbisCommentDecoderTest { + + @Test + public void decode() { + VorbisCommentDecoder decoder = new VorbisCommentDecoder(); + ArrayList commentsList = new ArrayList<>(); + + commentsList.add("Title=Test"); + commentsList.add("Artist=Test2"); + + Metadata metadata = decoder.decodeVorbisComments(commentsList); + + assertThat(metadata.length()).isEqualTo(2); + VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Title"); + assertThat(commentFrame.value).isEqualTo("Test"); + commentFrame = (VorbisCommentFrame) metadata.get(1); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Test2"); + } + + @Test + public void decodeEmptyList() { + VorbisCommentDecoder decoder = new VorbisCommentDecoder(); + ArrayList commentsList = new ArrayList<>(); + + Metadata metadata = decoder.decodeVorbisComments(commentsList); + + assertThat(metadata).isNull(); + } + + @Test + public void decodeTwoSeparators() { + VorbisCommentDecoder decoder = new VorbisCommentDecoder(); + ArrayList commentsList = new ArrayList<>(); + + commentsList.add("Title=Test"); + commentsList.add("Artist=Test=2"); + + Metadata metadata = decoder.decodeVorbisComments(commentsList); + + assertThat(metadata.length()).isEqualTo(1); + VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Title"); + assertThat(commentFrame.value).isEqualTo("Test"); + } + + @Test + public void decodeNoSeparators() { + VorbisCommentDecoder decoder = new VorbisCommentDecoder(); + ArrayList commentsList = new ArrayList<>(); + + commentsList.add("TitleTest"); + commentsList.add("Artist=Test2"); + + Metadata metadata = decoder.decodeVorbisComments(commentsList); + + assertThat(metadata.length()).isEqualTo(1); + VorbisCommentFrame commentFrame = (VorbisCommentFrame) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Test2"); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrameTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrameTest.java new file mode 100644 index 0000000000..218de9649d --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/vorbis/VorbisCommentFrameTest.java @@ -0,0 +1,43 @@ +/* + * 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 VorbisCommentFrame}. */ +@RunWith(AndroidJUnit4.class) +public final class VorbisCommentFrameTest { + + @Test + public void testParcelable() { + VorbisCommentFrame vorbisCommentFrameToParcel = new VorbisCommentFrame("key", "value"); + + Parcel parcel = Parcel.obtain(); + vorbisCommentFrameToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + VorbisCommentFrame vorbisCommentFrameFromParcel = + VorbisCommentFrame.CREATOR.createFromParcel(parcel); + assertThat(vorbisCommentFrameFromParcel).isEqualTo(vorbisCommentFrameToParcel); + + parcel.recycle(); + } +}