Another baby step to unified extractors.

- Have extractors read from an ExtractorInput. Benefits of this are
  (a) The ability to do a "full" read or skip of a specified number
  of bytes, (b) The ability to do multiple reads in your extractor's
  read method. The ExtractorInput will throw an InterruptedException
  if the read has been canceled.

- Provides the extractor with the ability to query the absolute
  position of the data being read in the stream. This is needed for
  things like parsing a segment index in fragmented mp4, where the
  position of the end of the box in the stream is required because
  the index offsets are all specified relative to that position.
This commit is contained in:
Oliver Woodman 2015-03-13 11:47:51 +00:00
parent 1111dd73a0
commit a22ccf9254
7 changed files with 206 additions and 56 deletions

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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.
* <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.
*
* @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;
}

View file

@ -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.
* <p>
* 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.

View file

@ -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;
}
/**

View file

@ -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}.
* <p>
* 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;