Commit 8a0af84c by hoangtc Committed by Andrew Lewis

Supports seeking for FLAC stream using binary search.

Added FlacBinarySearchSeeker, which supports seeking in a FLAC stream by searching for individual frames within the file using binary search.

Github: #1808.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=196587198
parent d3d4b33c
/*
* Copyright (C) 2018 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.ext.flac;
import static com.google.common.truth.Truth.assertThat;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
/** Unit test for {@link FlacBinarySearchSeeker}. */
public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
private static final String NOSEEKTABLE_FLAC = "bear_no_seek.flac";
private static final int DURATION_US = 2_741_000;
@Override
protected void setUp() throws Exception {
super.setUp();
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
}
public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
throws IOException, FlacDecoderException, InterruptedException {
byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni();
decoderJni.setData(input);
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull();
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
assertThat(seekMap.isSeekable()).isTrue();
}
public void testSetSeekTargetUs_returnsSeekPending()
throws IOException, FlacDecoderException, InterruptedException {
byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni();
decoderJni.setData(input);
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.hasPendingSeek()).isTrue();
}
}
...@@ -92,18 +92,14 @@ import java.util.List; ...@@ -92,18 +92,14 @@ import java.util.List;
} }
decoderJni.setData(inputBuffer.data); decoderJni.setData(inputBuffer.data);
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize);
int result;
try { try {
result = decoderJni.decodeSample(outputData); decoderJni.decodeSample(outputData);
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
return new FlacDecoderException("Frame decoding failed", e);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
// Never happens. // Never happens.
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
if (result < 0) {
return new FlacDecoderException("Frame decoding failed");
}
outputData.position(0);
outputData.limit(result);
return null; return null;
} }
......
...@@ -26,6 +26,17 @@ import java.nio.ByteBuffer; ...@@ -26,6 +26,17 @@ import java.nio.ByteBuffer;
*/ */
/* package */ final class FlacDecoderJni { /* package */ final class FlacDecoderJni {
/** Exception to be thrown if {@link #decodeSample(ByteBuffer)} fails to decode a frame. */
public static final class FlacFrameDecodeException extends Exception {
public final int errorCode;
public FlacFrameDecodeException(String message, int errorCode) {
super(message);
this.errorCode = errorCode;
}
}
private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has
private final long nativeDecoderContext; private final long nativeDecoderContext;
...@@ -116,14 +127,50 @@ import java.nio.ByteBuffer; ...@@ -116,14 +127,50 @@ import java.nio.ByteBuffer;
return byteCount; return byteCount;
} }
/** Decodes and consumes the StreamInfo section from the FLAC stream. */
public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException {
return flacDecodeMetadata(nativeDecoderContext); return flacDecodeMetadata(nativeDecoderContext);
} }
public int decodeSample(ByteBuffer output) throws IOException, InterruptedException { /**
return output.isDirect() * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO
? flacDecodeToBuffer(nativeDecoderContext, output) * error occurs, resets the stream and input to the given {@code retryPosition}.
: flacDecodeToArray(nativeDecoderContext, output.array()); *
* @param output The byte buffer to hold the decoded frame.
* @param retryPosition If any error happens, the input will be rewound to {@code retryPosition}.
*/
public void decodeSampleWithBacktrackPosition(ByteBuffer output, long retryPosition)
throws InterruptedException, IOException, FlacFrameDecodeException {
try {
decodeSample(output);
} catch (IOException e) {
if (retryPosition >= 0) {
reset(retryPosition);
if (extractorInput != null) {
extractorInput.setRetryPosition(retryPosition, e);
}
}
throw e;
}
}
/** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */
public void decodeSample(ByteBuffer output)
throws IOException, InterruptedException, FlacFrameDecodeException {
output.clear();
int frameSize =
output.isDirect()
? flacDecodeToBuffer(nativeDecoderContext, output)
: flacDecodeToArray(nativeDecoderContext, output.array());
if (frameSize < 0) {
if (!isDecoderAtEndOfInput()) {
throw new FlacFrameDecodeException("Cannot decode FLAC frame", frameSize);
}
// The decoder has read to EOI. Return a 0-size frame to indicate the EOI.
output.limit(0);
} else {
output.limit(frameSize);
}
} }
/** /**
...@@ -133,8 +180,19 @@ import java.nio.ByteBuffer; ...@@ -133,8 +180,19 @@ import java.nio.ByteBuffer;
return flacGetDecodePosition(nativeDecoderContext); return flacGetDecodePosition(nativeDecoderContext);
} }
public long getLastSampleTimestamp() { /** Returns the timestamp for the first sample in the last decoded frame. */
return flacGetLastTimestamp(nativeDecoderContext); public long getLastFrameTimestamp() {
return flacGetLastFrameTimestamp(nativeDecoderContext);
}
/** Returns the first sample index of the last extracted frame. */
public long getLastFrameFirstSampleIndex() {
return flacGetLastFrameFirstSampleIndex(nativeDecoderContext);
}
/** Returns the first sample index of the frame to be extracted next. */
public long getNextFrameFirstSampleIndex() {
return flacGetNextFrameFirstSampleIndex(nativeDecoderContext);
} }
/** /**
...@@ -153,6 +211,11 @@ import java.nio.ByteBuffer; ...@@ -153,6 +211,11 @@ import java.nio.ByteBuffer;
return flacGetStateString(nativeDecoderContext); return flacGetStateString(nativeDecoderContext);
} }
/** Returns whether the decoder has read to the end of the input. */
public boolean isDecoderAtEndOfInput() {
return flacIsDecoderAtEndOfStream(nativeDecoderContext);
}
public void flush() { public void flush() {
flacFlush(nativeDecoderContext); flacFlush(nativeDecoderContext);
} }
...@@ -181,18 +244,34 @@ import java.nio.ByteBuffer; ...@@ -181,18 +244,34 @@ import java.nio.ByteBuffer;
} }
private native long flacInit(); private native long flacInit();
private native FlacStreamInfo flacDecodeMetadata(long context) private native FlacStreamInfo 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)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native int flacDecodeToArray(long context, byte[] outputArray) private native int flacDecodeToArray(long context, byte[] outputArray)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native long flacGetDecodePosition(long context); private native long flacGetDecodePosition(long context);
private native long flacGetLastTimestamp(long context);
private native long flacGetLastFrameTimestamp(long context);
private native long flacGetLastFrameFirstSampleIndex(long context);
private native long flacGetNextFrameFirstSampleIndex(long context);
private native long flacGetSeekPosition(long context, long timeUs); private native long flacGetSeekPosition(long context, long timeUs);
private native String flacGetStateString(long context); private native String flacGetStateString(long context);
private native boolean flacIsDecoderAtEndOfStream(long context);
private native void flacFlush(long context); private native void flacFlush(long context);
private native void flacReset(long context, long newPosition); private native void flacReset(long context, long newPosition);
private native void flacRelease(long context); private native void flacRelease(long context);
} }
...@@ -179,24 +179,20 @@ public final class FlacExtractor implements Extractor { ...@@ -179,24 +179,20 @@ public final class FlacExtractor implements Extractor {
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data); outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
} }
outputBuffer.reset();
long lastDecodePosition = decoderJni.getDecodePosition(); long lastDecodePosition = decoderJni.getDecodePosition();
int size;
try { try {
size = decoderJni.decodeSample(outputByteBuffer); decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
} catch (IOException e) { } catch (FlacDecoderJni.FlacFrameDecodeException e) {
if (lastDecodePosition >= 0) { throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
decoderJni.reset(lastDecodePosition);
input.setRetryPosition(lastDecodePosition, e);
}
throw e;
} }
if (size <= 0) { int outputSize = outputByteBuffer.limit();
if (outputSize == 0) {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
} }
trackOutput.sampleData(outputBuffer, size); outputBuffer.setPosition(0);
trackOutput.sampleMetadata(decoderJni.getLastSampleTimestamp(), C.BUFFER_FLAG_KEY_FRAME, size, trackOutput.sampleData(outputBuffer, outputSize);
0, null); trackOutput.sampleMetadata(
decoderJni.getLastFrameTimestamp(), C.BUFFER_FLAG_KEY_FRAME, outputSize, 0, null);
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
} }
......
...@@ -133,9 +133,19 @@ DECODER_FUNC(jlong, flacGetDecodePosition, jlong jContext) { ...@@ -133,9 +133,19 @@ DECODER_FUNC(jlong, flacGetDecodePosition, jlong jContext) {
return context->parser->getDecodePosition(); return context->parser->getDecodePosition();
} }
DECODER_FUNC(jlong, flacGetLastTimestamp, jlong jContext) { DECODER_FUNC(jlong, flacGetLastFrameTimestamp, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext); Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getLastTimestamp(); return context->parser->getLastFrameTimestamp();
}
DECODER_FUNC(jlong, flacGetLastFrameFirstSampleIndex, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getLastFrameFirstSampleIndex();
}
DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getNextFrameFirstSampleIndex();
} }
DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) { DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) {
...@@ -149,6 +159,11 @@ DECODER_FUNC(jstring, flacGetStateString, jlong jContext) { ...@@ -149,6 +159,11 @@ DECODER_FUNC(jstring, flacGetStateString, jlong jContext) {
return env->NewStringUTF(str); return env->NewStringUTF(str);
} }
DECODER_FUNC(jboolean, flacIsDecoderAtEndOfStream, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->isDecoderAtEndOfStream();
}
DECODER_FUNC(void, flacFlush, jlong jContext) { DECODER_FUNC(void, flacFlush, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext); Context *context = reinterpret_cast<Context *>(jContext);
context->parser->flush(); context->parser->flush();
......
...@@ -44,10 +44,18 @@ class FLACParser { ...@@ -44,10 +44,18 @@ class FLACParser {
return mStreamInfo; return mStreamInfo;
} }
int64_t getLastTimestamp() const { int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
} }
int64_t getLastFrameFirstSampleIndex() const {
return mWriteHeader.number.sample_number;
}
int64_t getNextFrameFirstSampleIndex() const {
return mWriteHeader.number.sample_number + mWriteHeader.blocksize;
}
bool decodeMetadata(); bool decodeMetadata();
size_t readBuffer(void *output, size_t output_size); size_t readBuffer(void *output, size_t output_size);
...@@ -83,6 +91,11 @@ class FLACParser { ...@@ -83,6 +91,11 @@ class FLACParser {
return FLAC__stream_decoder_get_resolved_state_string(mDecoder); return FLAC__stream_decoder_get_resolved_state_string(mDecoder);
} }
bool isDecoderAtEndOfStream() const {
return FLAC__stream_decoder_get_state(mDecoder) ==
FLAC__STREAM_DECODER_END_OF_STREAM;
}
private: private:
DataSource *mDataSource; DataSource *mDataSource;
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import com.google.android.exoplayer2.C;
/** /**
* Holder for FLAC stream info. * Holder for FLAC stream info.
*/ */
...@@ -52,8 +54,29 @@ public final class FlacStreamInfo { ...@@ -52,8 +54,29 @@ public final class FlacStreamInfo {
// Remaining 16 bytes is md5 value // Remaining 16 bytes is md5 value
} }
public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize, /**
int sampleRate, int channels, int bitsPerSample, long totalSamples) { * 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.
* @param maxFrameSize Maximum frame size of the FLAC stream.
* @param sampleRate Sample rate of the FLAC stream.
* @param channels Number of channels of the FLAC stream.
* @param bitsPerSample Number of bits per sample of the FLAC stream.
* @param totalSamples Total samples of the FLAC stream.
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a>
*/
public FlacStreamInfo(
int minBlockSize,
int maxBlockSize,
int minFrameSize,
int maxFrameSize,
int sampleRate,
int channels,
int bitsPerSample,
long totalSamples) {
this.minBlockSize = minBlockSize; this.minBlockSize = minBlockSize;
this.maxBlockSize = maxBlockSize; this.maxBlockSize = maxBlockSize;
this.minFrameSize = minFrameSize; this.minFrameSize = minFrameSize;
...@@ -64,16 +87,43 @@ public final class FlacStreamInfo { ...@@ -64,16 +87,43 @@ public final class FlacStreamInfo {
this.totalSamples = totalSamples; this.totalSamples = totalSamples;
} }
/** Returns the maximum size for a decoded frame from the FLAC stream. */
public int maxDecodedFrameSize() { public int maxDecodedFrameSize() {
return maxBlockSize * channels * (bitsPerSample / 8); return maxBlockSize * channels * (bitsPerSample / 8);
} }
/** Returns the bit-rate of the FLAC stream. */
public int bitRate() { public int bitRate() {
return bitsPerSample * sampleRate; return bitsPerSample * sampleRate;
} }
/** Returns the duration of the FLAC stream in microseconds. */
public long durationUs() { public long durationUs() {
return (totalSamples * 1000000L) / sampleRate; return (totalSamples * 1000000L) / sampleRate;
} }
/**
* Returns the sample index for the sample at given position.
*
* @param timeUs Time position in microseconds in the FLAC stream.
* @return The sample index for the sample at given position.
*/
public long getSampleIndex(long timeUs) {
long sampleIndex = (timeUs * sampleRate) / C.MICROS_PER_SECOND;
return Util.constrainValue(sampleIndex, 0, totalSamples - 1);
}
/** Returns the approximate number of bytes per frame for the current FLAC stream. */
public long getApproxBytesPerFrame() {
long approxBytesPerFrame;
if (maxFrameSize > 0) {
approxBytesPerFrame = ((long) maxFrameSize + minFrameSize) / 2 + 1;
} else {
// Uses the stream's block-size if it's a known fixed block-size stream, otherwise uses the
// default value for FLAC block-size, which is 4096.
long blockSize = (minBlockSize == maxBlockSize && minBlockSize > 0) ? minBlockSize : 4096;
approxBytesPerFrame = (blockSize * channels * bitsPerSample) / 8 + 64;
}
return approxBytesPerFrame;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment