diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java index 863808d4ef..3b9e4c8000 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.hls; +import com.google.android.exoplayer.hls.parser.DataSourceExtractorInput; +import com.google.android.exoplayer.hls.parser.HlsExtractor.ExtractorInput; import com.google.android.exoplayer.hls.parser.HlsExtractorWrapper; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; @@ -26,8 +28,6 @@ import java.io.IOException; */ public final class TsChunk extends HlsChunk { - private static final byte[] SCRATCH_SPACE = new byte[4096]; - /** * The index of the variant in the master playlist. */ @@ -102,30 +102,23 @@ public final class TsChunk extends HlsChunk { @Override public void load() throws IOException, InterruptedException { + ExtractorInput input = new DataSourceExtractorInput(dataSource, 0); try { dataSource.open(dataSpec); - int bytesRead = 0; - int bytesSkipped = 0; // If we previously fed part of this chunk to the extractor, skip it this time. // TODO: Ideally we'd construct a dataSpec that only loads the remainder of the data here, // rather than loading the whole chunk again and then skipping data we previously loaded. To // do this is straightforward for non-encrypted content, but more complicated for content // encrypted with AES, for which we'll need to modify the way that decryption is performed. - while (bytesRead != -1 && !loadCanceled && bytesSkipped < loadPosition) { - int skipLength = Math.min(loadPosition - bytesSkipped, SCRATCH_SPACE.length); - bytesRead = dataSource.read(SCRATCH_SPACE, 0, skipLength); - if (bytesRead != -1) { - bytesSkipped += bytesRead; + input.skipFully(loadPosition); + try { + while (!input.isEnded() && !loadCanceled) { + extractor.read(input); } + } finally { + loadPosition = (int) input.getPosition(); + loadFinished = !loadCanceled; } - // Feed the remaining data into the extractor. - while (bytesRead != -1 && !loadCanceled) { - bytesRead = extractor.read(dataSource); - if (bytesRead != -1) { - loadPosition += bytesRead; - } - } - loadFinished = !loadCanceled; } finally { dataSource.close(); } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java index 5915ea6885..f286afac92 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer.hls.parser; -import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.ParsableByteArray; import java.io.IOException; @@ -48,12 +47,13 @@ public class AdtsExtractor implements HlsExtractor { } @Override - public int read(DataSource dataSource) throws IOException { - int bytesRead = dataSource.read(packetBuffer.data, 0, MAX_PACKET_SIZE); + public void read(ExtractorInput input) throws IOException, InterruptedException { + int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE); if (bytesRead == -1) { - return -1; + return; } + // Feed whatever data we have to the reader, regardless of whether the read finished or not. packetBuffer.setPosition(0); packetBuffer.setLimit(bytesRead); @@ -61,7 +61,6 @@ public class AdtsExtractor implements HlsExtractor { // unnecessary to copy the data through packetBuffer. adtsReader.consume(packetBuffer, firstSampleTimestamp, firstPacket); firstPacket = false; - return bytesRead; } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/DataSourceExtractorInput.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/DataSourceExtractorInput.java new file mode 100644 index 0000000000..f951bea7a3 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/DataSourceExtractorInput.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2014 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.exoplayer.hls.parser; + +import com.google.android.exoplayer.hls.parser.HlsExtractor.ExtractorInput; +import com.google.android.exoplayer.upstream.DataSource; + +import java.io.IOException; + +/** + * An {@link ExtractorInput} that wraps a {@link DataSource}. + */ +public final class DataSourceExtractorInput implements ExtractorInput { + + private static final byte[] SCRATCH_SPACE = new byte[4096]; + + private final DataSource dataSource; + + private long position; + private boolean isEnded; + + /** + * @param dataSource The wrapped {@link DataSource}. + * @param position The initial position in the stream. + */ + public DataSourceExtractorInput(DataSource dataSource, long position) { + this.dataSource = dataSource; + this.position = position; + } + + @Override + public int read(byte[] target, int offset, int length) throws IOException, InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + int bytesRead = dataSource.read(target, offset, length); + if (bytesRead == -1) { + isEnded = true; + return -1; + } + position += bytesRead; + return bytesRead; + } + + @Override + public boolean readFully(byte[] target, int offset, int length) + throws IOException, InterruptedException { + int remaining = length; + while (remaining > 0) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + int bytesRead = dataSource.read(target, offset, remaining); + if (bytesRead == -1) { + isEnded = true; + return false; + } + offset += bytesRead; + remaining -= bytesRead; + } + position += length; + return true; + } + + @Override + public boolean skipFully(int length) throws IOException, InterruptedException { + int remaining = length; + while (remaining > 0) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + int bytesRead = dataSource.read(SCRATCH_SPACE, 0, remaining); + if (bytesRead == -1) { + isEnded = true; + return true; + } + remaining -= bytesRead; + } + position += length; + return false; + } + + @Override + public long getPosition() { + return position; + } + + @Override + public boolean isEnded() { + return isEnded; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java index e3b3e5468f..c5c5790147 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java @@ -26,6 +26,72 @@ import java.io.IOException; */ public interface HlsExtractor { + /** + * An object from which source data can be read. + */ + public interface ExtractorInput { + + /** + * Reads up to {@code length} bytes from the input. + *
+ * 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. + * + * @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. + * @return The number of bytes read, or -1 if the input has ended. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + int read(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Like {@link #read(byte[], int, int)}, but guaranteed to read request {@code length} in full + * unless the end of the input is detected, or an exception is thrown. + * + * TODO: Firm up behavior of this method if (a) zero bytes are read before EOS, (b) the read + * is partially satisfied before EOS. + * + * @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 number of bytes to read from the input. + * @return True if the read was successful. False if the end of the input was reached. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + boolean readFully(byte[] target, int offset, int length) + throws IOException, InterruptedException; + + /** + * Like {@link #readFully(byte[], int, int)}, except the data is skipped instead of read. + * + * TODO: Firm up behavior of this method if (a) zero bytes are skipped before EOS, (b) the skip + * is partially satisfied before EOS. + * + * @param length The number of bytes to skip from the input. + * @return True if the read was successful. False if the end of the input was reached. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + boolean skipFully(int length) throws IOException, InterruptedException; + + /** + * The current position in the stream. + * + * @return The position in the stream. + */ + long getPosition(); + + /** + * Whether or not the input has ended. + * + * @return True if the input has ended. False otherwise. + */ + boolean isEnded(); + + } + /** * An object to which extracted data should be output. */ @@ -76,12 +142,12 @@ public interface HlsExtractor { void init(TrackOutputBuilder output); /** - * Reads up to a single TS packet. + * Reads from the provided {@link ExtractorInput}. * - * @param dataSource The {@link DataSource} from which to read. + * @param input The {@link ExtractorInput} from which to read. * @throws IOException If an error occurred reading from the source. - * @return The number of bytes read from the source. + * @throws InterruptedException If the thread was interrupted. */ - int read(DataSource dataSource) throws IOException; + void read(ExtractorInput input) throws IOException, InterruptedException; } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractorWrapper.java index 40bc8f7280..25dcd14961 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractorWrapper.java @@ -17,9 +17,9 @@ package com.google.android.exoplayer.hls.parser; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.hls.parser.HlsExtractor.ExtractorInput; import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput; import com.google.android.exoplayer.upstream.BufferPool; -import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.Assertions; import android.util.SparseArray; @@ -125,7 +125,7 @@ public final class HlsExtractorWrapper implements HlsExtractor.TrackOutputBuilde /** * Releases the extractor, recycling any pending or incomplete samples to the sample pool. *
- * This method should not be called whilst {@link #read(DataSource)} is also being invoked. + * This method should not be called whilst {@link #read(ExtractorInput)} is also being invoked. */ public void release() { for (int i = 0; i < sampleQueues.size(); i++) { @@ -183,14 +183,14 @@ public final class HlsExtractorWrapper implements HlsExtractor.TrackOutputBuilde } /** - * Reads up to a single TS packet. + * Reads from the provided {@link ExtractorInput}. * - * @param dataSource The {@link DataSource} from which to read. + * @param input The {@link ExtractorInput} from which to read. * @throws IOException If an error occurred reading from the source. - * @return The number of bytes read from the source. + * @throws InterruptedException If the thread was interrupted. */ - public int read(DataSource dataSource) throws IOException { - return extractor.read(dataSource); + public void read(ExtractorInput input) throws IOException, InterruptedException { + extractor.read(input); } // ExtractorOutput implementation. diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java index 4a4417270a..a7955ccf99 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer.hls.parser; import com.google.android.exoplayer.C; -import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableByteArray; @@ -51,7 +50,6 @@ public final class TsExtractor implements HlsExtractor { // Accessed only by the loading thread. private TrackOutputBuilder output; - private int tsPacketBytesRead; private long timestampOffsetUs; private long lastPts; @@ -71,27 +69,15 @@ public final class TsExtractor implements HlsExtractor { } @Override - public int read(DataSource dataSource) throws IOException { - int bytesRead = dataSource.read(tsPacketBuffer.data, tsPacketBytesRead, - TS_PACKET_SIZE - tsPacketBytesRead); - if (bytesRead == -1) { - return -1; + public void read(ExtractorInput input) throws IOException, InterruptedException { + if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE)) { + return; } - tsPacketBytesRead += bytesRead; - if (tsPacketBytesRead < TS_PACKET_SIZE) { - // We haven't read the whole packet yet. - return bytesRead; - } - - // Reset before reading the packet. - tsPacketBytesRead = 0; tsPacketBuffer.setPosition(0); - tsPacketBuffer.setLimit(TS_PACKET_SIZE); - int syncByte = tsPacketBuffer.readUnsignedByte(); if (syncByte != TS_SYNC_BYTE) { - return bytesRead; + return; } tsPacketBuffer.readBytes(tsScratch, 3); @@ -117,8 +103,6 @@ public final class TsExtractor implements HlsExtractor { payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator, output); } } - - return bytesRead; } /** diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java index 624e42a111..21f3488f7f 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java @@ -55,13 +55,15 @@ public interface DataSource { /** * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at - * index {@code offset}. This method blocks until at least one byte of data can be read, the end - * of the opened range is detected, or an exception is thrown. + * index {@code offset}. + *
+ * This method blocks until at least one byte of data can be read, the end of the opened range is + * detected, or an exception is thrown. * * @param buffer The buffer into which the read data should be stored. * @param offset The start offset into {@code buffer} at which data should be written. * @param readLength The maximum number of bytes to read. - * @return The actual number of bytes read, or -1 if the end of the opened range is reached. + * @return The number of bytes read, or -1 if the end of the opened range is reached. * @throws IOException If an error occurs reading from the source. */ public int read(byte[] buffer, int offset, int readLength) throws IOException;