Merge pull request #6151 from ittiam-systems:bug-5527
PiperOrigin-RevId: 257668797
This commit is contained in:
parent
b6777e030e
commit
bba0a27cb6
@ -3,6 +3,8 @@
|
||||
### 2.10.4 ###
|
||||
|
||||
* Offline: Add Scheduler implementation which uses WorkManager.
|
||||
* Flac extension: Parse `VORBIS_COMMENT` metadata
|
||||
([#5527](https://github.com/google/ExoPlayer/issues/5527)).
|
||||
|
||||
### 2.10.3 ###
|
||||
|
||||
|
@ -9,6 +9,6 @@
|
||||
-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 {
|
||||
*;
|
||||
}
|
||||
|
@ -52,7 +52,10 @@ public final class FlacBinarySearchSeekerTest {
|
||||
|
||||
FlacBinarySearchSeeker seeker =
|
||||
new FlacBinarySearchSeeker(
|
||||
decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni);
|
||||
decoderJni.decodeStreamMetadata(),
|
||||
/* firstFramePosition= */ 0,
|
||||
data.length,
|
||||
decoderJni);
|
||||
|
||||
SeekMap seekMap = seeker.getSeekMap();
|
||||
assertThat(seekMap).isNotNull();
|
||||
@ -70,7 +73,10 @@ public final class FlacBinarySearchSeekerTest {
|
||||
decoderJni.setData(input);
|
||||
FlacBinarySearchSeeker seeker =
|
||||
new FlacBinarySearchSeeker(
|
||||
decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni);
|
||||
decoderJni.decodeStreamMetadata(),
|
||||
/* firstFramePosition= */ 0,
|
||||
data.length,
|
||||
decoderJni);
|
||||
|
||||
seeker.setSeekTargetUs(/* timeUs= */ 1000);
|
||||
assertThat(seeker.isSeeking()).isTrue();
|
||||
|
@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
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.nio.ByteBuffer;
|
||||
|
||||
@ -34,20 +34,20 @@ import java.nio.ByteBuffer;
|
||||
private final FlacDecoderJni decoderJni;
|
||||
|
||||
public FlacBinarySearchSeeker(
|
||||
FlacStreamInfo streamInfo,
|
||||
FlacStreamMetadata streamMetadata,
|
||||
long firstFramePosition,
|
||||
long inputLength,
|
||||
FlacDecoderJni decoderJni) {
|
||||
super(
|
||||
new FlacSeekTimestampConverter(streamInfo),
|
||||
new FlacSeekTimestampConverter(streamMetadata),
|
||||
new FlacTimestampSeeker(decoderJni),
|
||||
streamInfo.durationUs(),
|
||||
streamMetadata.durationUs(),
|
||||
/* floorTimePosition= */ 0,
|
||||
/* ceilingTimePosition= */ streamInfo.totalSamples,
|
||||
/* ceilingTimePosition= */ streamMetadata.totalSamples,
|
||||
/* floorBytePosition= */ firstFramePosition,
|
||||
/* ceilingBytePosition= */ inputLength,
|
||||
/* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(),
|
||||
/* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize));
|
||||
/* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
|
||||
/* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize));
|
||||
this.decoderJni = Assertions.checkNotNull(decoderJni);
|
||||
}
|
||||
|
||||
@ -112,15 +112,15 @@ import java.nio.ByteBuffer;
|
||||
* the timestamp for a stream seek time position.
|
||||
*/
|
||||
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
|
||||
private final FlacStreamInfo streamInfo;
|
||||
private final FlacStreamMetadata streamMetadata;
|
||||
|
||||
public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) {
|
||||
this.streamMetadata = streamMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long timeUsToTargetTime(long timeUs) {
|
||||
return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs);
|
||||
return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||
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.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
@ -58,9 +58,9 @@ import java.util.List;
|
||||
}
|
||||
decoderJni = new FlacDecoderJni();
|
||||
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
|
||||
FlacStreamInfo streamInfo;
|
||||
FlacStreamMetadata streamMetadata;
|
||||
try {
|
||||
streamInfo = decoderJni.decodeStreamInfo();
|
||||
streamMetadata = decoderJni.decodeStreamMetadata();
|
||||
} catch (ParserException e) {
|
||||
throw new FlacDecoderException("Failed to decode StreamInfo", e);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
@ -69,9 +69,9 @@ import java.util.List;
|
||||
}
|
||||
|
||||
int initialInputBufferSize =
|
||||
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize;
|
||||
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
|
||||
setInitialInputBufferSize(initialInputBufferSize);
|
||||
maxOutputBufferSize = streamInfo.maxDecodedFrameSize();
|
||||
maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -19,7 +19,7 @@ import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
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 java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
@ -142,13 +142,13 @@ import java.nio.ByteBuffer;
|
||||
return byteCount;
|
||||
}
|
||||
|
||||
/** Decodes and consumes the StreamInfo section from the FLAC stream. */
|
||||
public FlacStreamInfo decodeStreamInfo() throws IOException, InterruptedException {
|
||||
FlacStreamInfo streamInfo = flacDecodeMetadata(nativeDecoderContext);
|
||||
if (streamInfo == null) {
|
||||
throw new ParserException("Failed to decode StreamInfo");
|
||||
/** Decodes and consumes the metadata from the FLAC stream. */
|
||||
public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException {
|
||||
FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext);
|
||||
if (streamMetadata == null) {
|
||||
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 FlacStreamInfo flacDecodeMetadata(long context)
|
||||
private native FlacStreamMetadata flacDecodeMetadata(long context)
|
||||
throws IOException, InterruptedException;
|
||||
|
||||
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
|
||||
|
@ -34,7 +34,7 @@ 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.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.ParsableByteArray;
|
||||
import java.io.IOException;
|
||||
@ -86,8 +86,8 @@ public final class FlacExtractor implements Extractor {
|
||||
private @MonotonicNonNull ExtractorOutput extractorOutput;
|
||||
private @MonotonicNonNull TrackOutput trackOutput;
|
||||
|
||||
private boolean streamInfoDecoded;
|
||||
private @MonotonicNonNull FlacStreamInfo streamInfo;
|
||||
private boolean streamMetadataDecoded;
|
||||
private @MonotonicNonNull FlacStreamMetadata streamMetadata;
|
||||
private @MonotonicNonNull OutputFrameHolder outputFrameHolder;
|
||||
|
||||
@Nullable private Metadata id3Metadata;
|
||||
@ -138,7 +138,7 @@ public final class FlacExtractor implements Extractor {
|
||||
|
||||
FlacDecoderJni decoderJni = initDecoderJni(input);
|
||||
try {
|
||||
decodeStreamInfo(input);
|
||||
decodeStreamMetadata(input);
|
||||
|
||||
if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
|
||||
return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput);
|
||||
@ -166,7 +166,7 @@ public final class FlacExtractor implements Extractor {
|
||||
@Override
|
||||
public void seek(long position, long timeUs) {
|
||||
if (position == 0) {
|
||||
streamInfoDecoded = false;
|
||||
streamMetadataDecoded = false;
|
||||
}
|
||||
if (decoderJni != null) {
|
||||
decoderJni.reset(position);
|
||||
@ -207,29 +207,33 @@ public final class FlacExtractor implements Extractor {
|
||||
}
|
||||
|
||||
@RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
|
||||
@EnsuresNonNull({"streamInfo", "outputFrameHolder"}) // Ensures StreamInfo decoded.
|
||||
@EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
|
||||
@SuppressWarnings({"contracts.postcondition.not.satisfied"})
|
||||
private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException {
|
||||
if (streamInfoDecoded) {
|
||||
private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException {
|
||||
if (streamMetadataDecoded) {
|
||||
return;
|
||||
}
|
||||
|
||||
FlacStreamInfo streamInfo;
|
||||
FlacStreamMetadata streamMetadata;
|
||||
try {
|
||||
streamInfo = decoderJni.decodeStreamInfo();
|
||||
streamMetadata = decoderJni.decodeStreamMetadata();
|
||||
} catch (IOException e) {
|
||||
decoderJni.reset(/* newPosition= */ 0);
|
||||
input.setRetryPosition(/* position= */ 0, e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
streamInfoDecoded = true;
|
||||
if (this.streamInfo == null) {
|
||||
this.streamInfo = streamInfo;
|
||||
streamMetadataDecoded = true;
|
||||
if (this.streamMetadata == null) {
|
||||
this.streamMetadata = streamMetadata;
|
||||
binarySearchSeeker =
|
||||
outputSeekMap(decoderJni, streamInfo, input.getLength(), extractorOutput);
|
||||
outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata, trackOutput);
|
||||
outputBuffer.reset(streamInfo.maxDecodedFrameSize());
|
||||
outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput);
|
||||
Metadata metadata = id3MetadataDisabled ? null : id3Metadata;
|
||||
if (streamMetadata.vorbisComments != null) {
|
||||
metadata = streamMetadata.vorbisComments.copyWithAppendedEntriesFrom(metadata);
|
||||
}
|
||||
outputFormat(streamMetadata, metadata, trackOutput);
|
||||
outputBuffer.reset(streamMetadata.maxDecodedFrameSize());
|
||||
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
|
||||
}
|
||||
}
|
||||
@ -269,38 +273,38 @@ public final class FlacExtractor implements Extractor {
|
||||
@Nullable
|
||||
private static FlacBinarySearchSeeker outputSeekMap(
|
||||
FlacDecoderJni decoderJni,
|
||||
FlacStreamInfo streamInfo,
|
||||
FlacStreamMetadata streamMetadata,
|
||||
long streamLength,
|
||||
ExtractorOutput output) {
|
||||
boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1;
|
||||
FlacBinarySearchSeeker binarySearchSeeker = null;
|
||||
SeekMap seekMap;
|
||||
if (hasSeekTable) {
|
||||
seekMap = new FlacSeekMap(streamInfo.durationUs(), decoderJni);
|
||||
seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);
|
||||
} else if (streamLength != C.LENGTH_UNSET) {
|
||||
long firstFramePosition = decoderJni.getDecodePosition();
|
||||
binarySearchSeeker =
|
||||
new FlacBinarySearchSeeker(streamInfo, firstFramePosition, streamLength, decoderJni);
|
||||
new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni);
|
||||
seekMap = binarySearchSeeker.getSeekMap();
|
||||
} else {
|
||||
seekMap = new SeekMap.Unseekable(streamInfo.durationUs());
|
||||
seekMap = new SeekMap.Unseekable(streamMetadata.durationUs());
|
||||
}
|
||||
output.seekMap(seekMap);
|
||||
return binarySearchSeeker;
|
||||
}
|
||||
|
||||
private static void outputFormat(
|
||||
FlacStreamInfo streamInfo, @Nullable Metadata metadata, TrackOutput output) {
|
||||
FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
|
||||
Format mediaFormat =
|
||||
Format.createAudioSampleFormat(
|
||||
/* id= */ null,
|
||||
MimeTypes.AUDIO_RAW,
|
||||
/* codecs= */ null,
|
||||
streamInfo.bitRate(),
|
||||
streamInfo.maxDecodedFrameSize(),
|
||||
streamInfo.channels,
|
||||
streamInfo.sampleRate,
|
||||
getPcmEncoding(streamInfo.bitsPerSample),
|
||||
streamMetadata.bitRate(),
|
||||
streamMetadata.maxDecodedFrameSize(),
|
||||
streamMetadata.channels,
|
||||
streamMetadata.sampleRate,
|
||||
getPcmEncoding(streamMetadata.bitsPerSample),
|
||||
/* encoderDelay= */ 0,
|
||||
/* encoderPadding= */ 0,
|
||||
/* initializationData= */ null,
|
||||
|
@ -14,9 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <jni.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "include/flac_parser.h"
|
||||
|
||||
#define LOG_TAG "flac_jni"
|
||||
@ -95,19 +98,40 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
|
||||
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 =
|
||||
context->parser->getStreamInfo();
|
||||
|
||||
jclass cls = env->FindClass(
|
||||
jclass flacStreamMetadataClass = env->FindClass(
|
||||
"com/google/android/exoplayer2/util/"
|
||||
"FlacStreamInfo");
|
||||
jmethodID constructor = env->GetMethodID(cls, "<init>", "(IIIIIIIJ)V");
|
||||
"FlacStreamMetadata");
|
||||
jmethodID flacStreamMetadataConstructor = env->GetMethodID(
|
||||
flacStreamMetadataClass, "<init>", "(IIIIIIIJLjava/util/List;)V");
|
||||
|
||||
return env->NewObject(cls, constructor, streamInfo.min_blocksize,
|
||||
streamInfo.max_blocksize, streamInfo.min_framesize,
|
||||
streamInfo.max_framesize, streamInfo.sample_rate,
|
||||
streamInfo.channels, streamInfo.bits_per_sample,
|
||||
streamInfo.total_samples);
|
||||
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);
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {
|
||||
|
@ -172,6 +172,25 @@ 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 (!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:
|
||||
ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type);
|
||||
break;
|
||||
@ -233,6 +252,7 @@ FLACParser::FLACParser(DataSource *source)
|
||||
mCurrentPos(0LL),
|
||||
mEOF(false),
|
||||
mStreamInfoValid(false),
|
||||
mVorbisCommentsValid(false),
|
||||
mWriteRequested(false),
|
||||
mWriteCompleted(false),
|
||||
mWriteBuffer(NULL),
|
||||
@ -266,6 +286,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,
|
||||
|
@ -19,6 +19,10 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// libFLAC parser
|
||||
#include "FLAC/stream_decoder.h"
|
||||
|
||||
@ -44,6 +48,10 @@ class FLACParser {
|
||||
return mStreamInfo;
|
||||
}
|
||||
|
||||
bool isVorbisCommentsValid() { return mVorbisCommentsValid; }
|
||||
|
||||
std::vector<std::string> getVorbisComments() { return mVorbisComments; }
|
||||
|
||||
int64_t getLastFrameTimestamp() const {
|
||||
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
|
||||
}
|
||||
@ -71,6 +79,8 @@ class FLACParser {
|
||||
mEOF = false;
|
||||
if (newPosition == 0) {
|
||||
mStreamInfoValid = false;
|
||||
mVorbisCommentsValid = false;
|
||||
mVorbisComments.clear();
|
||||
FLAC__stream_decoder_reset(mDecoder);
|
||||
} else {
|
||||
FLAC__stream_decoder_flush(mDecoder);
|
||||
@ -116,6 +126,10 @@ class FLACParser {
|
||||
const FLAC__StreamMetadata_SeekTable *mSeekTable;
|
||||
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
|
||||
bool mWriteRequested;
|
||||
bool mWriteCompleted;
|
||||
|
@ -19,7 +19,7 @@ import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
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.ParsableByteArray;
|
||||
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 FlacStreamInfo streamInfo;
|
||||
private FlacStreamMetadata streamMetadata;
|
||||
private FlacOggSeeker flacOggSeeker;
|
||||
|
||||
public static boolean verifyBitstreamType(ParsableByteArray data) {
|
||||
@ -50,7 +50,7 @@ import java.util.List;
|
||||
protected void reset(boolean headerData) {
|
||||
super.reset(headerData);
|
||||
if (headerData) {
|
||||
streamInfo = null;
|
||||
streamMetadata = null;
|
||||
flacOggSeeker = null;
|
||||
}
|
||||
}
|
||||
@ -71,14 +71,24 @@ import java.util.List;
|
||||
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
|
||||
throws IOException, InterruptedException {
|
||||
byte[] data = packet.data;
|
||||
if (streamInfo == null) {
|
||||
streamInfo = new FlacStreamInfo(data, 17);
|
||||
if (streamMetadata == null) {
|
||||
streamMetadata = new FlacStreamMetadata(data, 17);
|
||||
byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
|
||||
metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks
|
||||
List<byte[]> initializationData = Collections.singletonList(metadata);
|
||||
setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null,
|
||||
Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate,
|
||||
initializationData, null, 0, null);
|
||||
setupData.format =
|
||||
Format.createAudioSampleFormat(
|
||||
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) {
|
||||
flacOggSeeker = new FlacOggSeeker();
|
||||
flacOggSeeker.parseSeekTable(packet);
|
||||
@ -211,7 +221,7 @@ import java.util.List;
|
||||
|
||||
@Override
|
||||
public long getDurationUs() {
|
||||
return streamInfo.durationUs();
|
||||
return streamMetadata.durationUs();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
@ -15,12 +15,17 @@
|
||||
*/
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Holder for FLAC stream info.
|
||||
*/
|
||||
public final class FlacStreamInfo {
|
||||
/** Holder for FLAC metadata. */
|
||||
public final class FlacStreamMetadata {
|
||||
|
||||
private static final String TAG = "FlacStreamMetadata";
|
||||
|
||||
public final int minBlockSize;
|
||||
public final int maxBlockSize;
|
||||
@ -30,16 +35,19 @@ public final class FlacStreamInfo {
|
||||
public final int channels;
|
||||
public final int bitsPerSample;
|
||||
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 offset Offset of the structure in the array
|
||||
* @param data An array containing binary FLAC stream info metadata.
|
||||
* @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
|
||||
* METADATA_BLOCK_STREAMINFO</a>
|
||||
*/
|
||||
public FlacStreamInfo(byte[] data, int offset) {
|
||||
public FlacStreamMetadata(byte[] data, int offset) {
|
||||
ParsableBitArray scratch = new ParsableBitArray(data);
|
||||
scratch.setPosition(offset * 8);
|
||||
this.minBlockSize = scratch.readBits(16);
|
||||
@ -49,14 +57,11 @@ public final class FlacStreamInfo {
|
||||
this.sampleRate = scratch.readBits(20);
|
||||
this.channels = scratch.readBits(3) + 1;
|
||||
this.bitsPerSample = scratch.readBits(5) + 1;
|
||||
this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32)
|
||||
| (scratch.readBits(32) & 0xFFFFFFFFL);
|
||||
// Remaining 16 bytes is md5 value
|
||||
this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL);
|
||||
this.vorbisComments = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a FlacStreamInfo given the parameters.
|
||||
*
|
||||
* @param minBlockSize Minimum block size of the FLAC stream.
|
||||
* @param maxBlockSize Maximum block 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 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.
|
||||
* @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>
|
||||
*/
|
||||
public FlacStreamInfo(
|
||||
public FlacStreamMetadata(
|
||||
int minBlockSize,
|
||||
int maxBlockSize,
|
||||
int minFrameSize,
|
||||
@ -76,7 +84,8 @@ public final class FlacStreamInfo {
|
||||
int sampleRate,
|
||||
int channels,
|
||||
int bitsPerSample,
|
||||
long totalSamples) {
|
||||
long totalSamples,
|
||||
List<String> vorbisComments) {
|
||||
this.minBlockSize = minBlockSize;
|
||||
this.maxBlockSize = maxBlockSize;
|
||||
this.minFrameSize = minFrameSize;
|
||||
@ -85,6 +94,7 @@ public final class FlacStreamInfo {
|
||||
this.channels = channels;
|
||||
this.bitsPerSample = bitsPerSample;
|
||||
this.totalSamples = totalSamples;
|
||||
this.vorbisComments = parseVorbisComments(vorbisComments);
|
||||
}
|
||||
|
||||
/** Returns the maximum size for a decoded frame from the FLAC stream. */
|
||||
@ -126,4 +136,24 @@ public final class FlacStreamInfo {
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for <code>ColorParser</code>. */
|
||||
/** Unit test for {@link ColorParser}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class ColorParserTest {
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user