mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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
This commit is contained in:
parent
d3d4b33cac
commit
8a0af84c42
9 changed files with 593 additions and 32 deletions
BIN
extensions/flac/src/androidTest/assets/bear_no_seek.flac
Normal file
BIN
extensions/flac/src/androidTest/assets/bear_no_seek.flac
Normal file
Binary file not shown.
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,340 @@
|
||||||
|
/*
|
||||||
|
* 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 android.support.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
|
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link SeekMap} implementation for FLAC stream using binary search.
|
||||||
|
*
|
||||||
|
* <p>This seeker performs seeking by using binary search within the stream, until it finds the
|
||||||
|
* frame that contains the target sample.
|
||||||
|
*/
|
||||||
|
/* package */ final class FlacBinarySearchSeeker {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When seeking within the source, if the offset is smaller than or equal to this value, the seek
|
||||||
|
* operation will be performed using a skip operation. Otherwise, the source will be reloaded at
|
||||||
|
* the new seek position.
|
||||||
|
*/
|
||||||
|
private static final long MAX_SKIP_BYTES = 256 * 1024;
|
||||||
|
|
||||||
|
private final FlacStreamInfo streamInfo;
|
||||||
|
private final FlacBinarySearchSeekMap seekMap;
|
||||||
|
private final FlacDecoderJni decoderJni;
|
||||||
|
|
||||||
|
private final long firstFramePosition;
|
||||||
|
private final long inputLength;
|
||||||
|
private final long approxBytesPerFrame;
|
||||||
|
|
||||||
|
private @Nullable SeekOperationParams pendingSeekOperationParams;
|
||||||
|
|
||||||
|
public FlacBinarySearchSeeker(
|
||||||
|
FlacStreamInfo streamInfo,
|
||||||
|
long firstFramePosition,
|
||||||
|
long inputLength,
|
||||||
|
FlacDecoderJni decoderJni) {
|
||||||
|
this.streamInfo = Assertions.checkNotNull(streamInfo);
|
||||||
|
this.decoderJni = Assertions.checkNotNull(decoderJni);
|
||||||
|
this.firstFramePosition = firstFramePosition;
|
||||||
|
this.inputLength = inputLength;
|
||||||
|
this.approxBytesPerFrame = streamInfo.getApproxBytesPerFrame();
|
||||||
|
|
||||||
|
pendingSeekOperationParams = null;
|
||||||
|
seekMap =
|
||||||
|
new FlacBinarySearchSeekMap(
|
||||||
|
streamInfo,
|
||||||
|
firstFramePosition,
|
||||||
|
inputLength,
|
||||||
|
streamInfo.durationUs(),
|
||||||
|
approxBytesPerFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the seek map for the wrapped FLAC stream. */
|
||||||
|
public SeekMap getSeekMap() {
|
||||||
|
return seekMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the target time in microseconds within the stream to seek to. */
|
||||||
|
public void setSeekTargetUs(long timeUs) {
|
||||||
|
if (pendingSeekOperationParams != null && pendingSeekOperationParams.seekTimeUs == timeUs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingSeekOperationParams =
|
||||||
|
new SeekOperationParams(
|
||||||
|
timeUs,
|
||||||
|
streamInfo.getSampleIndex(timeUs),
|
||||||
|
/* floorSample= */ 0,
|
||||||
|
/* ceilingSample= */ streamInfo.totalSamples,
|
||||||
|
/* floorPosition= */ firstFramePosition,
|
||||||
|
/* ceilingPosition= */ inputLength,
|
||||||
|
approxBytesPerFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */
|
||||||
|
public boolean hasPendingSeek() {
|
||||||
|
return pendingSeekOperationParams != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from
|
||||||
|
* {@link Extractor}.
|
||||||
|
*
|
||||||
|
* @param input The {@link ExtractorInput} from which data should be read.
|
||||||
|
* @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
|
||||||
|
* to hold the position of the required seek.
|
||||||
|
* @param outputBuffer If {@link Extractor#RESULT_CONTINUE} is returned, this byte buffer maybe
|
||||||
|
* updated to hold the extracted frame that contains the target sample. The caller needs to
|
||||||
|
* check the byte buffer limit to see if an extracted frame is available.
|
||||||
|
* @return One of the {@code RESULT_} values defined in {@link Extractor}.
|
||||||
|
* @throws IOException If an error occurred reading from the input.
|
||||||
|
* @throws InterruptedException If the thread was interrupted.
|
||||||
|
*/
|
||||||
|
public int handlePendingSeek(
|
||||||
|
ExtractorInput input, PositionHolder seekPositionHolder, ByteBuffer outputBuffer)
|
||||||
|
throws InterruptedException, IOException {
|
||||||
|
outputBuffer.position(0);
|
||||||
|
outputBuffer.limit(0);
|
||||||
|
while (true) {
|
||||||
|
long floorPosition = pendingSeekOperationParams.floorPosition;
|
||||||
|
long ceilingPosition = pendingSeekOperationParams.ceilingPosition;
|
||||||
|
long searchPosition = pendingSeekOperationParams.nextSearchPosition;
|
||||||
|
|
||||||
|
// streamInfo may not contain minFrameSize, in which case this value will be 0.
|
||||||
|
int minFrameSize = Math.max(1, streamInfo.minFrameSize);
|
||||||
|
if (floorPosition + minFrameSize >= ceilingPosition) {
|
||||||
|
// The seeking range is too small for more than 1 frame, so we can just continue from
|
||||||
|
// the floor position.
|
||||||
|
pendingSeekOperationParams = null;
|
||||||
|
decoderJni.reset(floorPosition);
|
||||||
|
return seekToPosition(input, floorPosition, seekPositionHolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skipInputUntilPosition(input, searchPosition)) {
|
||||||
|
return seekToPosition(input, searchPosition, seekPositionHolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
decoderJni.reset(searchPosition);
|
||||||
|
try {
|
||||||
|
decoderJni.decodeSampleWithBacktrackPosition(
|
||||||
|
outputBuffer, /* retryPosition= */ searchPosition);
|
||||||
|
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
||||||
|
// For some reasons, the extractor can't find a frame mid-stream.
|
||||||
|
// Stop the seeking and let it re-try playing at the last search position.
|
||||||
|
pendingSeekOperationParams = null;
|
||||||
|
throw new IOException("Cannot read frame at position " + searchPosition, e);
|
||||||
|
}
|
||||||
|
if (outputBuffer.limit() == 0) {
|
||||||
|
return Extractor.RESULT_END_OF_INPUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex();
|
||||||
|
long nextFrameSampleIndex = decoderJni.getNextFrameFirstSampleIndex();
|
||||||
|
long nextFrameSamplePosition = decoderJni.getDecodePosition();
|
||||||
|
|
||||||
|
boolean targetSampleInLastFrame =
|
||||||
|
lastFrameSampleIndex <= pendingSeekOperationParams.targetSample
|
||||||
|
&& nextFrameSampleIndex > pendingSeekOperationParams.targetSample;
|
||||||
|
|
||||||
|
if (targetSampleInLastFrame) {
|
||||||
|
pendingSeekOperationParams = null;
|
||||||
|
return Extractor.RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextFrameSampleIndex <= pendingSeekOperationParams.targetSample) {
|
||||||
|
pendingSeekOperationParams.updateSeekFloor(nextFrameSampleIndex, nextFrameSamplePosition);
|
||||||
|
} else {
|
||||||
|
pendingSeekOperationParams.updateSeekCeiling(lastFrameSampleIndex, searchPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean skipInputUntilPosition(ExtractorInput input, long position)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
long bytesToSkip = position - input.getPosition();
|
||||||
|
if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) {
|
||||||
|
input.skipFully((int) bytesToSkip);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int seekToPosition(
|
||||||
|
ExtractorInput input, long position, PositionHolder seekPositionHolder) {
|
||||||
|
if (position == input.getPosition()) {
|
||||||
|
return Extractor.RESULT_CONTINUE;
|
||||||
|
} else {
|
||||||
|
seekPositionHolder.position = position;
|
||||||
|
return Extractor.RESULT_SEEK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains parameters for a pending seek operation by {@link FlacBinarySearchSeeker}.
|
||||||
|
*
|
||||||
|
* <p>This class holds parameters for a binary-search for the {@code targetSample} in the range
|
||||||
|
* [floorPosition, ceilingPosition).
|
||||||
|
*/
|
||||||
|
private static final class SeekOperationParams {
|
||||||
|
private final long seekTimeUs;
|
||||||
|
private final long targetSample;
|
||||||
|
private final long approxBytesPerFrame;
|
||||||
|
private long floorSample;
|
||||||
|
private long ceilingSample;
|
||||||
|
private long floorPosition;
|
||||||
|
private long ceilingPosition;
|
||||||
|
private long nextSearchPosition;
|
||||||
|
|
||||||
|
private SeekOperationParams(
|
||||||
|
long seekTimeUs,
|
||||||
|
long targetSample,
|
||||||
|
long floorSample,
|
||||||
|
long ceilingSample,
|
||||||
|
long floorPosition,
|
||||||
|
long ceilingPosition,
|
||||||
|
long approxBytesPerFrame) {
|
||||||
|
this.seekTimeUs = seekTimeUs;
|
||||||
|
this.floorSample = floorSample;
|
||||||
|
this.ceilingSample = ceilingSample;
|
||||||
|
this.floorPosition = floorPosition;
|
||||||
|
this.ceilingPosition = ceilingPosition;
|
||||||
|
this.targetSample = targetSample;
|
||||||
|
this.approxBytesPerFrame = approxBytesPerFrame;
|
||||||
|
updateNextSearchPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the floor constraints (inclusive) of the seek operation. */
|
||||||
|
private void updateSeekFloor(long floorSample, long floorPosition) {
|
||||||
|
this.floorSample = floorSample;
|
||||||
|
this.floorPosition = floorPosition;
|
||||||
|
updateNextSearchPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the ceiling constraints (exclusive) of the seek operation. */
|
||||||
|
private void updateSeekCeiling(long ceilingSample, long ceilingPosition) {
|
||||||
|
this.ceilingSample = ceilingSample;
|
||||||
|
this.ceilingPosition = ceilingPosition;
|
||||||
|
updateNextSearchPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNextSearchPosition() {
|
||||||
|
this.nextSearchPosition =
|
||||||
|
getNextSearchPosition(
|
||||||
|
targetSample,
|
||||||
|
floorSample,
|
||||||
|
ceilingSample,
|
||||||
|
floorPosition,
|
||||||
|
ceilingPosition,
|
||||||
|
approxBytesPerFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next position in FLAC stream to search for target sample, given [floorPosition,
|
||||||
|
* ceilingPosition).
|
||||||
|
*/
|
||||||
|
private static long getNextSearchPosition(
|
||||||
|
long targetSample,
|
||||||
|
long floorSample,
|
||||||
|
long ceilingSample,
|
||||||
|
long floorPosition,
|
||||||
|
long ceilingPosition,
|
||||||
|
long approxBytesPerFrame) {
|
||||||
|
if (floorPosition + 1 >= ceilingPosition || floorSample + 1 >= ceilingSample) {
|
||||||
|
return floorPosition;
|
||||||
|
}
|
||||||
|
long samplesToSkip = targetSample - floorSample;
|
||||||
|
long estimatedBytesPerSample =
|
||||||
|
Math.max(1, (ceilingPosition - floorPosition) / (ceilingSample - floorSample));
|
||||||
|
// In the stream, the samples are accessed in a group of frame. Given a stream position, the
|
||||||
|
// seeker will be able to find the first frame following that position.
|
||||||
|
// Hence, if our target sample is in the middle of a frame, and our estimate position is
|
||||||
|
// correct, or very near the actual sample position, the seeker will keep accessing the next
|
||||||
|
// frame, rather than the frame that contains the target sample.
|
||||||
|
// Moreover, it's better to under-estimate rather than over-estimate, because the extractor
|
||||||
|
// input can skip forward easily, but cannot rewind easily (it may require a new connection
|
||||||
|
// to be made).
|
||||||
|
// Therefore, we should reduce the estimated position by some amount, so it will converge to
|
||||||
|
// the correct frame earlier.
|
||||||
|
long bytesToSkip = samplesToSkip * estimatedBytesPerSample;
|
||||||
|
long confidenceInterval = bytesToSkip / 20;
|
||||||
|
|
||||||
|
long estimatedFramePosition = floorPosition + bytesToSkip - (approxBytesPerFrame - 1);
|
||||||
|
long estimatedPosition = estimatedFramePosition - confidenceInterval;
|
||||||
|
|
||||||
|
return Util.constrainValue(estimatedPosition, floorPosition, ceilingPosition - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link SeekMap} implementation that returns the estimated byte location from {@link
|
||||||
|
* SeekOperationParams#getNextSearchPosition(long, long, long, long, long, long)} for each {@link
|
||||||
|
* #getSeekPoints(long)} query.
|
||||||
|
*/
|
||||||
|
private static final class FlacBinarySearchSeekMap implements SeekMap {
|
||||||
|
private final FlacStreamInfo streamInfo;
|
||||||
|
private final long firstFramePosition;
|
||||||
|
private final long inputLength;
|
||||||
|
private final long approxBytesPerFrame;
|
||||||
|
private final long durationUs;
|
||||||
|
|
||||||
|
private FlacBinarySearchSeekMap(
|
||||||
|
FlacStreamInfo streamInfo,
|
||||||
|
long firstFramePosition,
|
||||||
|
long inputLength,
|
||||||
|
long durationUs,
|
||||||
|
long approxBytesPerFrame) {
|
||||||
|
this.streamInfo = streamInfo;
|
||||||
|
this.firstFramePosition = firstFramePosition;
|
||||||
|
this.inputLength = inputLength;
|
||||||
|
this.approxBytesPerFrame = approxBytesPerFrame;
|
||||||
|
this.durationUs = durationUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSeekable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SeekPoints getSeekPoints(long timeUs) {
|
||||||
|
long nextSearchPosition =
|
||||||
|
SeekOperationParams.getNextSearchPosition(
|
||||||
|
streamInfo.getSampleIndex(timeUs),
|
||||||
|
/* floorSample= */ 0,
|
||||||
|
/* ceilingSample= */ streamInfo.totalSamples,
|
||||||
|
/* floorPosition= */ firstFramePosition,
|
||||||
|
/* ceilingPosition= */ inputLength,
|
||||||
|
approxBytesPerFrame);
|
||||||
|
return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDurationUs() {
|
||||||
|
return durationUs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
*/
|
*/
|
||||||
/* 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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
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) {
|
||||||
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) {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 @@
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
// 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 {
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue