blob: 4ab306a2344cbb63f28269abef405b1de922d5de [file] [log] [blame]
/*
* Copyright (C) 2016 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.extractor;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataReader;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Arrays;
/** An {@link ExtractorInput} that wraps a {@link DataReader}. */
public final class DefaultExtractorInput implements ExtractorInput {
private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024;
private static final int PEEK_MAX_FREE_SPACE = 512 * 1024;
private static final int SCRATCH_SPACE_SIZE = 4096;
private final byte[] scratchSpace;
private final DataReader dataReader;
private final long streamLength;
private long position;
private byte[] peekBuffer;
private int peekBufferPosition;
private int peekBufferLength;
/**
* @param dataReader The wrapped {@link DataReader}.
* @param position The initial position in the stream.
* @param length The length of the stream, or {@link C#LENGTH_UNSET} if it is unknown.
*/
public DefaultExtractorInput(DataReader dataReader, long position, long length) {
this.dataReader = dataReader;
this.position = position;
this.streamLength = length;
peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
scratchSpace = new byte[SCRATCH_SPACE_SIZE];
}
@Override
public int read(byte[] target, int offset, int length) throws IOException {
int bytesRead = readFromPeekBuffer(target, offset, length);
if (bytesRead == 0) {
bytesRead =
readFromUpstream(
target, offset, length, /* bytesAlreadyRead= */ 0, /* allowEndOfInput= */ true);
}
commitBytesRead(bytesRead);
return bytesRead;
}
@Override
public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException {
int bytesRead = readFromPeekBuffer(target, offset, length);
while (bytesRead < length && bytesRead != C.RESULT_END_OF_INPUT) {
bytesRead = readFromUpstream(target, offset, length, bytesRead, allowEndOfInput);
}
commitBytesRead(bytesRead);
return bytesRead != C.RESULT_END_OF_INPUT;
}
@Override
public void readFully(byte[] target, int offset, int length) throws IOException {
readFully(target, offset, length, false);
}
@Override
public int skip(int length) throws IOException {
int bytesSkipped = skipFromPeekBuffer(length);
if (bytesSkipped == 0) {
bytesSkipped =
readFromUpstream(scratchSpace, 0, Math.min(length, scratchSpace.length), 0, true);
}
commitBytesRead(bytesSkipped);
return bytesSkipped;
}
@Override
public boolean skipFully(int length, boolean allowEndOfInput) throws IOException {
int bytesSkipped = skipFromPeekBuffer(length);
while (bytesSkipped < length && bytesSkipped != C.RESULT_END_OF_INPUT) {
int minLength = Math.min(length, bytesSkipped + scratchSpace.length);
bytesSkipped =
readFromUpstream(scratchSpace, -bytesSkipped, minLength, bytesSkipped, allowEndOfInput);
}
commitBytesRead(bytesSkipped);
return bytesSkipped != C.RESULT_END_OF_INPUT;
}
@Override
public void skipFully(int length) throws IOException {
skipFully(length, false);
}
@Override
public int peek(byte[] target, int offset, int length) throws IOException {
ensureSpaceForPeek(length);
int peekBufferRemainingBytes = peekBufferLength - peekBufferPosition;
int bytesPeeked;
if (peekBufferRemainingBytes == 0) {
bytesPeeked =
readFromUpstream(
peekBuffer,
peekBufferPosition,
length,
/* bytesAlreadyRead= */ 0,
/* allowEndOfInput= */ true);
if (bytesPeeked == C.RESULT_END_OF_INPUT) {
return C.RESULT_END_OF_INPUT;
}
peekBufferLength += bytesPeeked;
} else {
bytesPeeked = Math.min(length, peekBufferRemainingBytes);
}
System.arraycopy(peekBuffer, peekBufferPosition, target, offset, bytesPeeked);
peekBufferPosition += bytesPeeked;
return bytesPeeked;
}
@Override
public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException {
if (!advancePeekPosition(length, allowEndOfInput)) {
return false;
}
System.arraycopy(peekBuffer, peekBufferPosition - length, target, offset, length);
return true;
}
@Override
public void peekFully(byte[] target, int offset, int length) throws IOException {
peekFully(target, offset, length, false);
}
@Override
public boolean advancePeekPosition(int length, boolean allowEndOfInput) throws IOException {
ensureSpaceForPeek(length);
int bytesPeeked = peekBufferLength - peekBufferPosition;
while (bytesPeeked < length) {
bytesPeeked =
readFromUpstream(peekBuffer, peekBufferPosition, length, bytesPeeked, allowEndOfInput);
if (bytesPeeked == C.RESULT_END_OF_INPUT) {
return false;
}
peekBufferLength = peekBufferPosition + bytesPeeked;
}
peekBufferPosition += length;
return true;
}
@Override
public void advancePeekPosition(int length) throws IOException {
advancePeekPosition(length, false);
}
@Override
public void resetPeekPosition() {
peekBufferPosition = 0;
}
@Override
public long getPeekPosition() {
return position + peekBufferPosition;
}
@Override
public long getPosition() {
return position;
}
@Override
public long getLength() {
return streamLength;
}
@Override
public <E extends Throwable> void setRetryPosition(long position, E e) throws E {
Assertions.checkArgument(position >= 0);
this.position = position;
throw e;
}
/**
* Ensures {@code peekBuffer} is large enough to store at least {@code length} bytes from the
* current peek position.
*/
private void ensureSpaceForPeek(int length) {
int requiredLength = peekBufferPosition + length;
if (requiredLength > peekBuffer.length) {
int newPeekCapacity = Util.constrainValue(peekBuffer.length * 2,
requiredLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE, requiredLength + PEEK_MAX_FREE_SPACE);
peekBuffer = Arrays.copyOf(peekBuffer, newPeekCapacity);
}
}
/**
* Skips from the peek buffer.
*
* @param length The maximum number of bytes to skip from the peek buffer.
* @return The number of bytes skipped.
*/
private int skipFromPeekBuffer(int length) {
int bytesSkipped = Math.min(peekBufferLength, length);
updatePeekBuffer(bytesSkipped);
return bytesSkipped;
}
/**
* Reads from the peek buffer.
*
* @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write.
* @param length The maximum number of bytes to read from the peek buffer.
* @return The number of bytes read.
*/
private int readFromPeekBuffer(byte[] target, int offset, int length) {
if (peekBufferLength == 0) {
return 0;
}
int peekBytes = Math.min(peekBufferLength, length);
System.arraycopy(peekBuffer, 0, target, offset, peekBytes);
updatePeekBuffer(peekBytes);
return peekBytes;
}
/**
* Updates the peek buffer's length, position and contents after consuming data.
*
* @param bytesConsumed The number of bytes consumed from the peek buffer.
*/
private void updatePeekBuffer(int bytesConsumed) {
peekBufferLength -= bytesConsumed;
peekBufferPosition = 0;
byte[] newPeekBuffer = peekBuffer;
if (peekBufferLength < peekBuffer.length - PEEK_MAX_FREE_SPACE) {
newPeekBuffer = new byte[peekBufferLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
}
System.arraycopy(peekBuffer, bytesConsumed, newPeekBuffer, 0, peekBufferLength);
peekBuffer = newPeekBuffer;
}
/**
* Starts or continues a read from the data reader.
*
* @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write.
* @param length The maximum number of bytes to read from the input.
* @param bytesAlreadyRead The number of bytes already read from the input.
* @param allowEndOfInput True if encountering the end of the input having read no data is
* allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it
* should be considered an error, causing an {@link EOFException} to be thrown.
* @return The total number of bytes read so far, or {@link C#RESULT_END_OF_INPUT} if {@code
* allowEndOfInput} is true and the input has ended having read no bytes.
* @throws EOFException If the end of input was encountered having partially satisfied the read
* (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were
* read and {@code allowEndOfInput} is false.
* @throws IOException If an error occurs reading from the input.
*/
private int readFromUpstream(
byte[] target, int offset, int length, int bytesAlreadyRead, boolean allowEndOfInput)
throws IOException {
if (Thread.interrupted()) {
throw new InterruptedIOException();
}
int bytesRead = dataReader.read(target, offset + bytesAlreadyRead, length - bytesAlreadyRead);
if (bytesRead == C.RESULT_END_OF_INPUT) {
if (bytesAlreadyRead == 0 && allowEndOfInput) {
return C.RESULT_END_OF_INPUT;
}
throw new EOFException();
}
return bytesAlreadyRead + bytesRead;
}
/**
* Advances the position by the specified number of bytes read.
*
* @param bytesRead The number of bytes read.
*/
private void commitBytesRead(int bytesRead) {
if (bytesRead != C.RESULT_END_OF_INPUT) {
position += bytesRead;
}
}
}