Merge pull request #6239 from ittiam-systems:vorbis-picture-parse

PiperOrigin-RevId: 261087432
This commit is contained in:
Oliver Woodman 2019-08-01 20:37:19 +01:00
parent e159e3acd0
commit 309d043cee
13 changed files with 323 additions and 32 deletions

View File

@ -16,7 +16,7 @@
([#6192](https://github.com/google/ExoPlayer/issues/6192)).
* Ensure the `SilenceMediaSource` position is in range
([#6229](https://github.com/google/ExoPlayer/issues/6229)).
* Flac extension: Parse `VORBIS_COMMENT` metadata
* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata
([#5527](https://github.com/google/ExoPlayer/issues/5527)).
### 2.10.3 ###

View File

@ -12,3 +12,6 @@
-keep class com.google.android.exoplayer2.util.FlacStreamMetadata {
*;
}
-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame {
*;
}

View File

@ -229,8 +229,8 @@ public final class FlacExtractor implements Extractor {
binarySearchSeeker =
outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput);
Metadata metadata = id3MetadataDisabled ? null : id3Metadata;
if (streamMetadata.vorbisComments != null) {
metadata = streamMetadata.vorbisComments.copyWithAppendedEntriesFrom(metadata);
if (streamMetadata.metadata != null) {
metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata);
}
outputFormat(streamMetadata, metadata, trackOutput);
outputBuffer.reset(streamMetadata.maxDecodedFrameSize());

View File

@ -102,10 +102,10 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
jmethodID arrayListConstructor =
env->GetMethodID(arrayListClass, "<init>", "()V");
jobject commentList = env->NewObject(arrayListClass, arrayListConstructor);
jmethodID arrayListAddMethod =
env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
if (context->parser->isVorbisCommentsValid()) {
jmethodID arrayListAddMethod =
env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
if (context->parser->areVorbisCommentsValid()) {
std::vector<std::string> vorbisComments =
context->parser->getVorbisComments();
for (std::vector<std::string>::const_iterator vorbisComment =
@ -117,21 +117,49 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
}
}
jobject pictureFrames = env->NewObject(arrayListClass, arrayListConstructor);
bool picturesValid = context->parser->arePicturesValid();
if (picturesValid) {
std::vector<FlacPicture> pictures = context->parser->getPictures();
jclass pictureFrameClass = env->FindClass(
"com/google/android/exoplayer2/metadata/flac/PictureFrame");
jmethodID pictureFrameConstructor =
env->GetMethodID(pictureFrameClass, "<init>",
"(ILjava/lang/String;Ljava/lang/String;IIII[B)V");
for (std::vector<FlacPicture>::const_iterator picture = pictures.begin();
picture != pictures.end(); ++picture) {
jstring mimeType = env->NewStringUTF(picture->mimeType.c_str());
jstring description = env->NewStringUTF(picture->description.c_str());
jbyteArray pictureData = env->NewByteArray(picture->data.size());
env->SetByteArrayRegion(pictureData, 0, picture->data.size(),
(signed char *)&picture->data[0]);
jobject pictureFrame = env->NewObject(
pictureFrameClass, pictureFrameConstructor, picture->type, mimeType,
description, picture->width, picture->height, picture->depth,
picture->colors, pictureData);
env->CallBooleanMethod(pictureFrames, arrayListAddMethod, pictureFrame);
env->DeleteLocalRef(mimeType);
env->DeleteLocalRef(description);
env->DeleteLocalRef(pictureData);
}
}
const FLAC__StreamMetadata_StreamInfo &streamInfo =
context->parser->getStreamInfo();
jclass flacStreamMetadataClass = env->FindClass(
"com/google/android/exoplayer2/util/"
"FlacStreamMetadata");
jmethodID flacStreamMetadataConstructor = env->GetMethodID(
flacStreamMetadataClass, "<init>", "(IIIIIIIJLjava/util/List;)V");
jmethodID flacStreamMetadataConstructor =
env->GetMethodID(flacStreamMetadataClass, "<init>",
"(IIIIIIIJLjava/util/List;Ljava/util/List;)V");
return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
streamInfo.min_blocksize, streamInfo.max_blocksize,
streamInfo.min_framesize, streamInfo.max_framesize,
streamInfo.sample_rate, streamInfo.channels,
streamInfo.bits_per_sample, streamInfo.total_samples,
commentList);
commentList, pictureFrames);
}
DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {

View File

@ -191,6 +191,24 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) {
ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT");
}
break;
case FLAC__METADATA_TYPE_PICTURE: {
const FLAC__StreamMetadata_Picture *parsedPicture =
&metadata->data.picture;
FlacPicture picture;
picture.mimeType.assign(std::string(parsedPicture->mime_type));
picture.description.assign(
std::string((char *)parsedPicture->description));
picture.data.assign(parsedPicture->data,
parsedPicture->data + parsedPicture->data_length);
picture.width = parsedPicture->width;
picture.height = parsedPicture->height;
picture.depth = parsedPicture->depth;
picture.colors = parsedPicture->colors;
picture.type = parsedPicture->type;
mPictures.push_back(picture);
mPicturesValid = true;
break;
}
default:
ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type);
break;
@ -253,6 +271,7 @@ FLACParser::FLACParser(DataSource *source)
mEOF(false),
mStreamInfoValid(false),
mVorbisCommentsValid(false),
mPicturesValid(false),
mWriteRequested(false),
mWriteCompleted(false),
mWriteBuffer(NULL),
@ -288,6 +307,8 @@ bool FLACParser::init() {
FLAC__METADATA_TYPE_SEEKTABLE);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_VORBIS_COMMENT);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_PICTURE);
FLAC__StreamDecoderInitStatus initStatus;
initStatus = FLAC__stream_decoder_init_stream(
mDecoder, read_callback, seek_callback, tell_callback, length_callback,

View File

@ -30,6 +30,17 @@
typedef int status_t;
struct FlacPicture {
int type;
std::string mimeType;
std::string description;
FLAC__uint32 width;
FLAC__uint32 height;
FLAC__uint32 depth;
FLAC__uint32 colors;
std::vector<char> data;
};
class FLACParser {
public:
FLACParser(DataSource *source);
@ -48,10 +59,14 @@ class FLACParser {
return mStreamInfo;
}
bool isVorbisCommentsValid() { return mVorbisCommentsValid; }
bool areVorbisCommentsValid() const { return mVorbisCommentsValid; }
std::vector<std::string> getVorbisComments() { return mVorbisComments; }
bool arePicturesValid() const { return mPicturesValid; }
const std::vector<FlacPicture> &getPictures() const { return mPictures; }
int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
}
@ -80,7 +95,9 @@ class FLACParser {
if (newPosition == 0) {
mStreamInfoValid = false;
mVorbisCommentsValid = false;
mPicturesValid = false;
mVorbisComments.clear();
mPictures.clear();
FLAC__stream_decoder_reset(mDecoder);
} else {
FLAC__stream_decoder_flush(mDecoder);
@ -130,6 +147,10 @@ class FLACParser {
std::vector<std::string> mVorbisComments;
bool mVorbisCommentsValid;
// cached when the PICTURE metadata is parsed by libFLAC
std::vector<FlacPicture> mPictures;
bool mPicturesValid;
// cached when a decoded PCM block is "written" by libFLAC parser
bool mWriteRequested;
bool mWriteCompleted;

View File

@ -0,0 +1,144 @@
/*
* 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.flac;
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;
import java.util.Arrays;
/** A picture parsed from a FLAC file. */
public final class PictureFrame implements Metadata.Entry {
/** The type of the picture. */
public final int pictureType;
/** The mime type of the picture. */
public final String mimeType;
/** A description of the picture. */
public final String description;
/** The width of the picture in pixels. */
public final int width;
/** The height of the picture in pixels. */
public final int height;
/** The color depth of the picture in bits-per-pixel. */
public final int depth;
/** For indexed-color pictures (e.g. GIF), the number of colors used. 0 otherwise. */
public final int colors;
/** The encoded picture data. */
public final byte[] pictureData;
public PictureFrame(
int pictureType,
String mimeType,
String description,
int width,
int height,
int depth,
int colors,
byte[] pictureData) {
this.pictureType = pictureType;
this.mimeType = mimeType;
this.description = description;
this.width = width;
this.height = height;
this.depth = depth;
this.colors = colors;
this.pictureData = pictureData;
}
/* package */ PictureFrame(Parcel in) {
this.pictureType = in.readInt();
this.mimeType = castNonNull(in.readString());
this.description = castNonNull(in.readString());
this.width = in.readInt();
this.height = in.readInt();
this.depth = in.readInt();
this.colors = in.readInt();
this.pictureData = castNonNull(in.createByteArray());
}
@Override
public String toString() {
return "Picture: mimeType=" + mimeType + ", description=" + description;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
PictureFrame other = (PictureFrame) obj;
return (pictureType == other.pictureType)
&& mimeType.equals(other.mimeType)
&& description.equals(other.description)
&& (width == other.width)
&& (height == other.height)
&& (depth == other.depth)
&& (colors == other.colors)
&& Arrays.equals(pictureData, other.pictureData);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + pictureType;
result = 31 * result + mimeType.hashCode();
result = 31 * result + description.hashCode();
result = 31 * result + width;
result = 31 * result + height;
result = 31 * result + depth;
result = 31 * result + colors;
result = 31 * result + Arrays.hashCode(pictureData);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(pictureType);
dest.writeString(mimeType);
dest.writeString(description);
dest.writeInt(width);
dest.writeInt(height);
dest.writeInt(depth);
dest.writeInt(colors);
dest.writeByteArray(pictureData);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<PictureFrame> CREATOR =
new Parcelable.Creator<PictureFrame>() {
@Override
public PictureFrame createFromParcel(Parcel in) {
return new PictureFrame(in);
}
@Override
public PictureFrame[] newArray(int size) {
return new PictureFrame[size];
}
};
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.vorbis;
package com.google.android.exoplayer2.metadata.flac;
import static com.google.android.exoplayer2.util.Util.castNonNull;

View File

@ -18,7 +18,8 @@ package com.google.android.exoplayer2.util;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.flac.VorbisComment;
import java.util.ArrayList;
import java.util.List;
@ -35,7 +36,7 @@ public final class FlacStreamMetadata {
public final int channels;
public final int bitsPerSample;
public final long totalSamples;
@Nullable public final Metadata vorbisComments;
@Nullable public final Metadata metadata;
private static final String SEPARATOR = "=";
@ -58,7 +59,7 @@ public final class FlacStreamMetadata {
this.channels = scratch.readBits(3) + 1;
this.bitsPerSample = scratch.readBits(5) + 1;
this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL);
this.vorbisComments = null;
this.metadata = null;
}
/**
@ -71,10 +72,13 @@ public final class FlacStreamMetadata {
* @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(
int minBlockSize,
@ -85,7 +89,8 @@ public final class FlacStreamMetadata {
int channels,
int bitsPerSample,
long totalSamples,
List<String> vorbisComments) {
List<String> vorbisComments,
List<PictureFrame> pictureFrames) {
this.minBlockSize = minBlockSize;
this.maxBlockSize = maxBlockSize;
this.minFrameSize = minFrameSize;
@ -94,7 +99,7 @@ public final class FlacStreamMetadata {
this.channels = channels;
this.bitsPerSample = bitsPerSample;
this.totalSamples = totalSamples;
this.vorbisComments = parseVorbisComments(vorbisComments);
this.metadata = buildMetadata(vorbisComments, pictureFrames);
}
/** Returns the maximum size for a decoded frame from the FLAC stream. */
@ -138,22 +143,25 @@ public final class FlacStreamMetadata {
}
@Nullable
private static Metadata parseVorbisComments(@Nullable List<String> vorbisComments) {
if (vorbisComments == null || vorbisComments.isEmpty()) {
private static Metadata buildMetadata(
List<String> vorbisComments, List<PictureFrame> pictureFrames) {
if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) {
return null;
}
ArrayList<VorbisComment> commentFrames = new ArrayList<>();
for (String vorbisComment : vorbisComments) {
ArrayList<Metadata.Entry> metadataEntries = new ArrayList<>();
for (int i = 0; i < vorbisComments.size(); i++) {
String vorbisComment = vorbisComments.get(i);
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);
VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]);
metadataEntries.add(entry);
}
}
metadataEntries.addAll(pictureFrames);
return commentFrames.isEmpty() ? null : new Metadata(commentFrames);
return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries);
}
}

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.flac;
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 PictureFrame}. */
@RunWith(AndroidJUnit4.class)
public final class PictureFrameTest {
@Test
public void testParcelable() {
PictureFrame pictureFrameToParcel = new PictureFrame(0, "", "", 0, 0, 0, 0, new byte[0]);
Parcel parcel = Parcel.obtain();
pictureFrameToParcel.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
PictureFrame pictureFrameFromParcel = PictureFrame.CREATOR.createFromParcel(parcel);
assertThat(pictureFrameFromParcel).isEqualTo(pictureFrameToParcel);
parcel.recycle();
}
}

View File

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

View File

@ -19,7 +19,7 @@ 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 com.google.android.exoplayer2.metadata.flac.VorbisComment;
import java.util.ArrayList;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -34,7 +34,8 @@ public final class FlacStreamMetadataTest {
commentsList.add("Title=Song");
commentsList.add("Artist=Singer");
Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments;
Metadata metadata =
new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata;
assertThat(metadata.length()).isEqualTo(2);
VorbisComment commentFrame = (VorbisComment) metadata.get(0);
@ -49,7 +50,8 @@ public final class FlacStreamMetadataTest {
public void parseEmptyVorbisComments() {
ArrayList<String> commentsList = new ArrayList<>();
Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments;
Metadata metadata =
new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata;
assertThat(metadata).isNull();
}
@ -59,7 +61,8 @@ public final class FlacStreamMetadataTest {
ArrayList<String> commentsList = new ArrayList<>();
commentsList.add("Title=So=ng");
Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments;
Metadata metadata =
new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata;
assertThat(metadata.length()).isEqualTo(1);
VorbisComment commentFrame = (VorbisComment) metadata.get(0);
@ -73,7 +76,8 @@ public final class FlacStreamMetadataTest {
commentsList.add("TitleSong");
commentsList.add("Artist=Singer");
Metadata metadata = new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList).vorbisComments;
Metadata metadata =
new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata;
assertThat(metadata.length()).isEqualTo(1);
VorbisComment commentFrame = (VorbisComment) metadata.get(0);

View File

@ -50,6 +50,7 @@ import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader;
@ -304,6 +305,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
private boolean controllerHideOnTouch;
private int textureViewRotation;
private boolean isTouching;
private static final int PICTURE_TYPE_FRONT_COVER = 3;
private static final int PICTURE_TYPE_NOT_SET = -1;
public PlayerView(Context context) {
this(context, null);
@ -1246,15 +1249,32 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
}
private boolean setArtworkFromMetadata(Metadata metadata) {
boolean isArtworkSet = false;
int currentPictureType = PICTURE_TYPE_NOT_SET;
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry metadataEntry = metadata.get(i);
int pictureType;
byte[] bitmapData;
if (metadataEntry instanceof ApicFrame) {
byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData;
bitmapData = ((ApicFrame) metadataEntry).pictureData;
pictureType = ((ApicFrame) metadataEntry).pictureType;
} else if (metadataEntry instanceof PictureFrame) {
bitmapData = ((PictureFrame) metadataEntry).pictureData;
pictureType = ((PictureFrame) metadataEntry).pictureType;
} else {
continue;
}
// Prefer the first front cover picture. If there aren't any, prefer the first picture.
if (currentPictureType == PICTURE_TYPE_NOT_SET || pictureType == PICTURE_TYPE_FRONT_COVER) {
Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length);
return setDrawableArtwork(new BitmapDrawable(getResources(), bitmap));
isArtworkSet = setDrawableArtwork(new BitmapDrawable(getResources(), bitmap));
currentPictureType = pictureType;
if (currentPictureType == PICTURE_TYPE_FRONT_COVER) {
break;
}
}
}
return false;
return isArtworkSet;
}
private boolean setDrawableArtwork(@Nullable Drawable drawable) {