Simplify FlacExtractor (step toward enabling nullness checking)

- Inline some unnecessarily split out helper methods
- Clear ExtractorInput from FlacDecoderJni data after usage
- Clean up exception handling for StreamInfo decode failures

PiperOrigin-RevId: 256524955
This commit is contained in:
olly 2019-07-04 11:23:52 +01:00 committed by Oliver Woodman
parent 67ad84f121
commit 98714235ad
5 changed files with 119 additions and 123 deletions

View File

@ -52,7 +52,7 @@ public final class FlacBinarySearchSeekerTest {
FlacBinarySearchSeeker seeker = FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker( new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni);
SeekMap seekMap = seeker.getSeekMap(); SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull(); assertThat(seekMap).isNotNull();
@ -70,7 +70,7 @@ public final class FlacBinarySearchSeekerTest {
decoderJni.setData(input); decoderJni.setData(input);
FlacBinarySearchSeeker seeker = FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker( new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); decoderJni.decodeStreamInfo(), /* firstFramePosition= */ 0, data.length, decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000); seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.isSeeking()).isTrue(); assertThat(seeker.isSeeking()).isTrue();

View File

@ -28,7 +28,7 @@ import org.junit.runner.RunWith;
public class FlacExtractorTest { public class FlacExtractorTest {
@Before @Before
public void setUp() throws Exception { public void setUp() {
if (!FlacLibrary.isAvailable()) { if (!FlacLibrary.isAvailable()) {
fail("Flac library not available."); fail("Flac library not available.");
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.flac;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
@ -59,14 +60,13 @@ import java.util.List;
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
FlacStreamInfo streamInfo; FlacStreamInfo streamInfo;
try { try {
streamInfo = decoderJni.decodeMetadata(); streamInfo = decoderJni.decodeStreamInfo();
} catch (ParserException e) {
throw new FlacDecoderException("Failed to decode StreamInfo", e);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
// Never happens. // Never happens.
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
if (streamInfo == null) {
throw new FlacDecoderException("Metadata decoding failed");
}
int initialInputBufferSize = int initialInputBufferSize =
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize; maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize;

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.flac;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
@ -48,7 +49,6 @@ import java.nio.ByteBuffer;
@Nullable private byte[] tempBuffer; @Nullable private byte[] tempBuffer;
private boolean endOfExtractorInput; private boolean endOfExtractorInput;
@SuppressWarnings("nullness:method.invocation.invalid")
public FlacDecoderJni() throws FlacDecoderException { public FlacDecoderJni() throws FlacDecoderException {
if (!FlacLibrary.isAvailable()) { if (!FlacLibrary.isAvailable()) {
throw new FlacDecoderException("Failed to load decoder native libraries."); throw new FlacDecoderException("Failed to load decoder native libraries.");
@ -60,37 +60,46 @@ import java.nio.ByteBuffer;
} }
/** /**
* Sets data to be parsed by libflac. * Sets the data to be parsed.
* *
* @param byteBufferData Source {@link ByteBuffer}. * @param byteBufferData Source {@link ByteBuffer}.
*/ */
public void setData(ByteBuffer byteBufferData) { public void setData(ByteBuffer byteBufferData) {
this.byteBufferData = byteBufferData; this.byteBufferData = byteBufferData;
this.extractorInput = null; this.extractorInput = null;
this.tempBuffer = null;
} }
/** /**
* Sets data to be parsed by libflac. * Sets the data to be parsed.
* *
* @param extractorInput Source {@link ExtractorInput}. * @param extractorInput Source {@link ExtractorInput}.
*/ */
public void setData(ExtractorInput extractorInput) { public void setData(ExtractorInput extractorInput) {
this.byteBufferData = null; this.byteBufferData = null;
this.extractorInput = extractorInput; this.extractorInput = extractorInput;
if (tempBuffer == null) {
this.tempBuffer = new byte[TEMP_BUFFER_SIZE];
}
endOfExtractorInput = false; endOfExtractorInput = false;
if (tempBuffer == null) {
tempBuffer = new byte[TEMP_BUFFER_SIZE];
}
} }
/**
* Returns whether the end of the data to be parsed has been reached, or true if no data was set.
*/
public boolean isEndOfData() { public boolean isEndOfData() {
if (byteBufferData != null) { if (byteBufferData != null) {
return byteBufferData.remaining() == 0; return byteBufferData.remaining() == 0;
} else if (extractorInput != null) { } else if (extractorInput != null) {
return endOfExtractorInput; return endOfExtractorInput;
} else {
return true;
} }
return true; }
/** Clears the data to be parsed. */
public void clearData() {
byteBufferData = null;
extractorInput = null;
} }
/** /**
@ -99,12 +108,11 @@ import java.nio.ByteBuffer;
* <p>This method blocks until at least one byte of data can be read, the end of the input is * <p>This method blocks until at least one byte of data can be read, the end of the input is
* detected or an exception is thrown. * detected or an exception is thrown.
* *
* <p>This method is called from the native code.
*
* @param target A target {@link ByteBuffer} into which data should be written. * @param target A target {@link ByteBuffer} into which data should be written.
* @return Returns the number of bytes read, or -1 on failure. If all of the data has already been * @return Returns the number of bytes read, or -1 on failure. If all of the data has already been
* read from the source, then 0 is returned. * read from the source, then 0 is returned.
*/ */
@SuppressWarnings("unused") // Called from native code.
public int read(ByteBuffer target) throws IOException, InterruptedException { public int read(ByteBuffer target) throws IOException, InterruptedException {
int byteCount = target.remaining(); int byteCount = target.remaining();
if (byteBufferData != null) { if (byteBufferData != null) {
@ -135,8 +143,12 @@ import java.nio.ByteBuffer;
} }
/** Decodes and consumes the StreamInfo section from the FLAC stream. */ /** Decodes and consumes the StreamInfo section from the FLAC stream. */
public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { public FlacStreamInfo decodeStreamInfo() throws IOException, InterruptedException {
return flacDecodeMetadata(nativeDecoderContext); FlacStreamInfo streamInfo = flacDecodeMetadata(nativeDecoderContext);
if (streamInfo == null) {
throw new ParserException("Failed to decode StreamInfo");
}
return streamInfo;
} }
/** /**

View File

@ -21,7 +21,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker; import com.google.android.exoplayer2.extractor.BinarySearchSeeker.OutputFrameHolder;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
@ -75,22 +75,19 @@ public final class FlacExtractor implements Extractor {
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
private final Id3Peeker id3Peeker; private final Id3Peeker id3Peeker;
private final boolean isId3MetadataDisabled; private final boolean id3MetadataDisabled;
private FlacDecoderJni decoderJni; @Nullable private FlacDecoderJni decoderJni;
@Nullable private ExtractorOutput extractorOutput;
@Nullable private TrackOutput trackOutput;
private ExtractorOutput extractorOutput; private boolean streamInfoDecoded;
private TrackOutput trackOutput; @Nullable private FlacStreamInfo streamInfo;
@Nullable private ParsableByteArray outputBuffer;
@Nullable private OutputFrameHolder outputFrameHolder;
private ParsableByteArray outputBuffer; @Nullable private Metadata id3Metadata;
private ByteBuffer outputByteBuffer; @Nullable private FlacBinarySearchSeeker binarySearchSeeker;
private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
private FlacStreamInfo streamInfo;
private Metadata id3Metadata;
private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
private boolean readPastStreamInfo;
/** Constructs an instance with flags = 0. */ /** Constructs an instance with flags = 0. */
public FlacExtractor() { public FlacExtractor() {
@ -104,7 +101,7 @@ public final class FlacExtractor implements Extractor {
*/ */
public FlacExtractor(int flags) { public FlacExtractor(int flags) {
id3Peeker = new Id3Peeker(); id3Peeker = new Id3Peeker();
isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
} }
@Override @Override
@ -130,48 +127,53 @@ public final class FlacExtractor implements Extractor {
@Override @Override
public int read(final ExtractorInput input, PositionHolder seekPosition) public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) { if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
id3Metadata = peekId3Data(input); id3Metadata = peekId3Data(input);
} }
decoderJni.setData(input); decoderJni.setData(input);
readPastStreamInfo(input);
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
return handlePendingSeek(input, seekPosition);
}
long lastDecodePosition = decoderJni.getDecodePosition();
try { try {
decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition); decodeStreamInfo(input);
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
}
int outputSize = outputByteBuffer.limit();
if (outputSize == 0) {
return RESULT_END_OF_INPUT;
}
writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp()); if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; return handlePendingSeek(input, seekPosition);
}
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
long lastDecodePosition = decoderJni.getDecodePosition();
try {
decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
}
int outputSize = outputByteBuffer.limit();
if (outputSize == 0) {
return RESULT_END_OF_INPUT;
}
outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp());
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
} finally {
decoderJni.clearData();
}
} }
@Override @Override
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
if (position == 0) { if (position == 0) {
readPastStreamInfo = false; streamInfoDecoded = false;
} }
if (decoderJni != null) { if (decoderJni != null) {
decoderJni.reset(position); decoderJni.reset(position);
} }
if (flacBinarySearchSeeker != null) { if (binarySearchSeeker != null) {
flacBinarySearchSeeker.setSeekTargetUs(timeUs); binarySearchSeeker.setSeekTargetUs(timeUs);
} }
} }
@Override @Override
public void release() { public void release() {
flacBinarySearchSeeker = null; binarySearchSeeker = null;
if (decoderJni != null) { if (decoderJni != null) {
decoderJni.release(); decoderJni.release();
decoderJni = null; decoderJni = null;
@ -179,16 +181,15 @@ public final class FlacExtractor implements Extractor {
} }
/** /**
* Peeks ID3 tag data (if present) at the beginning of the input. * Peeks ID3 tag data at the beginning of the input.
* *
* @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not * @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input.
* present in the input.
*/ */
@Nullable @Nullable
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException { private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition(); input.resetPeekPosition();
Id3Decoder.FramePredicate id3FramePredicate = Id3Decoder.FramePredicate id3FramePredicate =
isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null; id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
return id3Peeker.peekId3Data(input, id3FramePredicate); return id3Peeker.peekId3Data(input, id3FramePredicate);
} }
@ -199,68 +200,61 @@ public final class FlacExtractor implements Extractor {
*/ */
private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException { private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException {
byte[] header = new byte[FLAC_SIGNATURE.length]; byte[] header = new byte[FLAC_SIGNATURE.length];
input.peekFully(header, 0, FLAC_SIGNATURE.length); input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length);
return Arrays.equals(header, FLAC_SIGNATURE); return Arrays.equals(header, FLAC_SIGNATURE);
} }
private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException { private void decodeStreamInfo(ExtractorInput input) throws InterruptedException, IOException {
if (readPastStreamInfo) { if (streamInfoDecoded) {
return; return;
} }
FlacStreamInfo streamInfo = decodeStreamInfo(input); FlacStreamInfo streamInfo;
readPastStreamInfo = true;
if (this.streamInfo == null) {
updateFlacStreamInfo(input, streamInfo);
}
}
private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) {
this.streamInfo = streamInfo;
outputSeekMap(input, streamInfo);
outputFormat(streamInfo);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
}
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
throws InterruptedException, IOException {
try { try {
FlacStreamInfo streamInfo = decoderJni.decodeMetadata(); streamInfo = decoderJni.decodeStreamInfo();
if (streamInfo == null) {
throw new IOException("Metadata decoding failed");
}
return streamInfo;
} catch (IOException e) { } catch (IOException e) {
decoderJni.reset(0); decoderJni.reset(/* newPosition= */ 0);
input.setRetryPosition(0, e); input.setRetryPosition(/* position= */ 0, e);
throw e; throw e;
} }
streamInfoDecoded = true;
if (this.streamInfo == null) {
this.streamInfo = streamInfo;
outputSeekMap(streamInfo, input.getLength());
outputFormat(streamInfo, id3MetadataDisabled ? null : id3Metadata);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
}
} }
private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) { private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1; throws InterruptedException, IOException {
SeekMap seekMap = int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
hasSeekTable ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
? new FlacSeekMap(streamInfo.durationUs(), decoderJni) if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
: getSeekMapForNonSeekTableFlac(input, streamInfo); outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs);
}
return seekResult;
}
private void outputSeekMap(FlacStreamInfo streamInfo, long inputLength) {
boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1;
SeekMap seekMap;
if (hasSeekTable) {
seekMap = new FlacSeekMap(streamInfo.durationUs(), decoderJni);
} else if (inputLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition();
binarySearchSeeker =
new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni);
seekMap = binarySearchSeeker.getSeekMap();
} else {
seekMap = new SeekMap.Unseekable(streamInfo.durationUs());
}
extractorOutput.seekMap(seekMap); extractorOutput.seekMap(seekMap);
} }
private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) { private void outputFormat(FlacStreamInfo streamInfo, Metadata metadata) {
long inputLength = input.getLength();
if (inputLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition();
flacBinarySearchSeeker =
new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni);
return flacBinarySearchSeeker.getSeekMap();
} else { // can't seek at all, because there's no SeekTable and the input length is unknown.
return new SeekMap.Unseekable(streamInfo.durationUs());
}
}
private void outputFormat(FlacStreamInfo streamInfo) {
Format mediaFormat = Format mediaFormat =
Format.createAudioSampleFormat( Format.createAudioSampleFormat(
/* id= */ null, /* id= */ null,
@ -277,25 +271,15 @@ public final class FlacExtractor implements Extractor {
/* drmInitData= */ null, /* drmInitData= */ null,
/* selectionFlags= */ 0, /* selectionFlags= */ 0,
/* language= */ null, /* language= */ null,
isId3MetadataDisabled ? null : id3Metadata); metadata);
trackOutput.format(mediaFormat); trackOutput.format(mediaFormat);
} }
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) private void outputSample(ParsableByteArray sampleData, int size, long timeUs) {
throws InterruptedException, IOException { sampleData.setPosition(0);
int seekResult = trackOutput.sampleData(sampleData, size);
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); trackOutput.sampleMetadata(
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null);
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
}
return seekResult;
}
private void writeLastSampleToOutput(int size, long lastSampleTimestamp) {
outputBuffer.setPosition(0);
trackOutput.sampleData(outputBuffer, size);
trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null);
} }
/** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */ /** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */