Refactor HLS support.

- The HlsSampleSource now owns the extractor. TsChunk is more or less dumb.
  The previous model was weird, because you'd end up "reading" samples from
  TsChunk objects that were actually parsed from the previous chunk (due to
  the way the extractor was shared and maintained internal queues).
- Split out consuming and reading in the extractor.
- Make it so we consume 5s ahead. This is a window we allow for uneven
  interleaving, whilst preventing huge read-ahead (e.g. in the case of sparse
  ID3 samples).
- Avoid flushing the extractor for a discontinuity until it has been fully
  drained of previously parsed samples. This avoids skipping media shortly
  before discontinuities.
- Also made start-up faster by avoiding double-loading the first segment.

Issue: #3
This commit is contained in:
Oliver Woodman 2014-10-28 19:25:10 +00:00
parent d3a05c9a44
commit 2422912be8
4 changed files with 183 additions and 210 deletions

View file

@ -18,7 +18,6 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.parser.ts.TsExtractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
@ -40,10 +39,10 @@ import java.util.List;
public class HlsChunkSource {
private final DataSource dataSource;
private final TsExtractor extractor;
private final HlsMasterPlaylist masterPlaylist;
private final HlsMediaPlaylistParser mediaPlaylistParser;
private long liveStartTimeUs;
/* package */ HlsMediaPlaylist mediaPlaylist;
/* package */ boolean mediaPlaylistWasLive;
/* package */ long lastMediaPlaylistLoadTimeMs;
@ -52,7 +51,6 @@ public class HlsChunkSource {
public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist) {
this.dataSource = dataSource;
this.masterPlaylist = masterPlaylist;
extractor = new TsExtractor();
mediaPlaylistParser = new HlsMediaPlaylistParser();
}
@ -120,6 +118,7 @@ public class HlsChunkSource {
}
}
} else {
// Not live.
if (queue.isEmpty()) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true,
true) + mediaPlaylist.mediaSequence;
@ -151,14 +150,26 @@ public class HlsChunkSource {
long startTimeUs = segment.startTimeUs;
long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000);
int nextChunkMediaSequence = chunkMediaSequence + 1;
if (!mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1) {
nextChunkMediaSequence = -1;
if (mediaPlaylistWasLive) {
if (queue.isEmpty()) {
liveStartTimeUs = startTimeUs;
startTimeUs = 0;
endTimeUs -= liveStartTimeUs;
} else {
startTimeUs -= liveStartTimeUs;
endTimeUs -= liveStartTimeUs;
}
} else {
// Not live.
if (chunkIndex == mediaPlaylist.segments.size() - 1) {
nextChunkMediaSequence = -1;
}
}
out.chunk = new TsChunk(dataSource, dataSpec, 0, extractor, startTimeUs, endTimeUs,
nextChunkMediaSequence, segment.discontinuity);
out.chunk = new TsChunk(dataSource, dataSpec, 0, startTimeUs, endTimeUs, nextChunkMediaSequence,
segment.discontinuity);
}
private boolean shouldRerequestMediaPlaylist() {

View file

@ -23,8 +23,10 @@ import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.parser.ts.TsExtractor;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import android.os.SystemClock;
@ -43,8 +45,10 @@ import java.util.List;
*/
public class HlsSampleSource implements SampleSource, Loader.Callback {
private static final long MAX_SAMPLE_INTERLEAVING_OFFSET_US = 5000000;
private static final int NO_RESET_PENDING = -1;
private final TsExtractor extractor;
private final LoadControl loadControl;
private final HlsChunkSource chunkSource;
private final HlsChunkOperationHolder currentLoadableHolder;
@ -73,9 +77,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
private boolean pendingTimestampOffsetUpdate;
private long timestampOffsetUs;
public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl,
int bufferSizeContribution, boolean frameAccurateSeeking, int downstreamRendererCount) {
this.chunkSource = chunkSource;
@ -83,6 +84,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
this.bufferSizeContribution = bufferSizeContribution;
this.frameAccurateSeeking = frameAccurateSeeking;
this.remainingReleaseCount = downstreamRendererCount;
extractor = new TsExtractor();
currentLoadableHolder = new HlsChunkOperationHolder();
mediaChunks = new LinkedList<TsChunk>();
readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks);
@ -93,32 +95,23 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
if (prepared) {
return true;
}
if (loader == null) {
loader = new Loader("Loader:HLS");
loadControl.register(this, bufferSizeContribution);
}
updateLoadControl();
if (mediaChunks.isEmpty()) {
return false;
}
TsChunk mediaChunk = mediaChunks.getFirst();
if (mediaChunk.prepare()) {
trackCount = mediaChunk.getTrackCount();
continueBufferingInternal();
if (extractor.isPrepared()) {
trackCount = extractor.getTrackCount();
trackEnabledStates = new boolean[trackCount];
pendingDiscontinuities = new boolean[trackCount];
downstreamMediaFormats = new MediaFormat[trackCount];
trackInfos = new TrackInfo[trackCount];
for (int i = 0; i < trackCount; i++) {
MediaFormat format = mediaChunk.getMediaFormat(i);
MediaFormat format = extractor.getFormat(i);
trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs());
}
prepared = true;
}
if (!prepared && currentLoadableException != null) {
throw currentLoadableException;
}
return prepared;
}
@ -142,9 +135,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
trackEnabledStates[track] = true;
downstreamMediaFormats[track] = null;
if (enabledTrackCount == 1) {
downstreamPositionUs = positionUs;
lastSeekPositionUs = positionUs;
restartFrom(positionUs);
seekToUs(positionUs);
}
}
@ -170,72 +161,69 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0);
downstreamPositionUs = playbackPositionUs;
return continueBufferingInternal();
}
private boolean continueBufferingInternal() throws IOException {
updateLoadControl();
if (isPendingReset() || mediaChunks.isEmpty()) {
if (isPendingReset()) {
return false;
} else if (mediaChunks.getFirst().sampleAvailable()) {
// There's a sample available to be read from the current chunk.
return true;
} else {
// It may be the case that the current chunk has been fully read but not yet discarded and
// that the next chunk has an available sample. Return true if so, otherwise false.
return mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable();
}
TsChunk mediaChunk = mediaChunks.getFirst();
if (mediaChunk.isReadFinished() && mediaChunks.size() > 1) {
discardDownstreamHlsChunk();
mediaChunk = mediaChunks.getFirst();
}
boolean haveSufficientSamples = false;
if (mediaChunk.hasPendingDiscontinuity()) {
if (extractor.hasSamples()) {
// There are samples from before the discontinuity yet to be read from the extractor, so
// we don't want to reset the extractor yet.
haveSufficientSamples = true;
} else {
extractor.reset(mediaChunk.startTimeUs);
for (int i = 0; i < pendingDiscontinuities.length; i++) {
pendingDiscontinuities[i] = true;
}
mediaChunk.clearPendingDiscontinuity();
}
}
if (!mediaChunk.hasPendingDiscontinuity()) {
// Allow the extractor to consume from the current chunk.
NonBlockingInputStream inputStream = mediaChunk.getNonBlockingInputStream();
haveSufficientSamples = extractor.consumeUntil(inputStream,
downstreamPositionUs + MAX_SAMPLE_INTERLEAVING_OFFSET_US);
// If we can't read any more, then we always say we have sufficient samples.
if (!haveSufficientSamples) {
haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished();
}
}
if (!haveSufficientSamples && currentLoadableException != null) {
throw currentLoadableException;
}
return haveSufficientSamples;
}
@Override
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) {
Assertions.checkState(prepared);
downstreamPositionUs = playbackPositionUs;
if (pendingDiscontinuities[track]) {
pendingDiscontinuities[track] = false;
return DISCONTINUITY_READ;
}
if (onlyReadDiscontinuity) {
if (onlyReadDiscontinuity || isPendingReset() || !extractor.isPrepared()) {
return NOTHING_READ;
}
downstreamPositionUs = playbackPositionUs;
if (isPendingReset()) {
if (currentLoadableException != null) {
throw currentLoadableException;
}
return NOTHING_READ;
}
TsChunk mediaChunk = mediaChunks.getFirst();
if (mediaChunk.readDiscontinuity()) {
pendingTimestampOffsetUpdate = true;
for (int i = 0; i < pendingDiscontinuities.length; i++) {
pendingDiscontinuities[i] = true;
}
pendingDiscontinuities[track] = false;
return DISCONTINUITY_READ;
}
if (mediaChunk.isReadFinished()) {
// We've read all of the samples from the current media chunk.
if (mediaChunks.size() > 1) {
discardDownstreamHlsChunk();
mediaChunk = mediaChunks.getFirst();
return readData(track, playbackPositionUs, formatHolder, sampleHolder, false);
} else if (mediaChunk.isLastChunk()) {
return END_OF_STREAM;
}
return NOTHING_READ;
}
if (!mediaChunk.prepare()) {
if (currentLoadableException != null) {
throw currentLoadableException;
}
return NOTHING_READ;
}
MediaFormat mediaFormat = mediaChunk.getMediaFormat(track);
MediaFormat mediaFormat = extractor.getFormat(track);
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormats[track], true)) {
chunkSource.getMaxVideoDimensions(mediaFormat);
formatHolder.format = mediaFormat;
@ -243,20 +231,17 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return FORMAT_READ;
}
if (mediaChunk.read(track, sampleHolder)) {
if (pendingTimestampOffsetUpdate) {
pendingTimestampOffsetUpdate = false;
timestampOffsetUs = sampleHolder.timeUs - mediaChunk.startTimeUs;
}
sampleHolder.timeUs -= timestampOffsetUs;
if (extractor.getSample(track, sampleHolder)) {
sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
return SAMPLE_READ;
} else {
if (currentLoadableException != null) {
throw currentLoadableException;
}
return NOTHING_READ;
}
TsChunk mediaChunk = mediaChunks.getFirst();
if (mediaChunk.isLastChunk() && mediaChunk.isReadFinished()) {
return END_OF_STREAM;
}
return NOTHING_READ;
}
@Override
@ -276,9 +261,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
if (mediaChunk == null) {
restartFrom(positionUs);
} else {
pendingTimestampOffsetUpdate = true;
mediaChunk.reset();
discardDownstreamHlsChunks(mediaChunk);
mediaChunk.reset();
extractor.reset(mediaChunk.startTimeUs);
updateLoadControl();
}
}
@ -503,12 +488,11 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
currentLoadable.init(loadControl.getAllocator());
if (isTsChunk(currentLoadable)) {
TsChunk mediaChunk = (TsChunk) currentLoadable;
mediaChunks.add(mediaChunk);
if (isPendingReset()) {
pendingTimestampOffsetUpdate = true;
mediaChunk.reset();
extractor.reset(mediaChunk.startTimeUs);
pendingResetPositionUs = NO_RESET_PENDING;
}
mediaChunks.add(mediaChunk);
}
loader.startLoading(currentLoadable, this);
}

View file

@ -15,12 +15,8 @@
*/
package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.ts.TsExtractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
/**
* A MPEG2TS chunk.
@ -44,82 +40,42 @@ public final class TsChunk extends HlsChunk {
*/
private final boolean discontinuity;
private final TsExtractor extractor;
private boolean pendingDiscontinuity;
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param extractor The extractor that will be used to extract the samples.
* @param trigger The reason for this chunk being selected.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
* @param discontinuity The encoding discontinuity indicator.
*/
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, TsExtractor extractor,
long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity) {
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, long startTimeUs,
long endTimeUs, int nextChunkIndex, boolean discontinuity) {
super(dataSource, dataSpec, trigger);
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
this.nextChunkIndex = nextChunkIndex;
this.extractor = extractor;
this.discontinuity = discontinuity;
this.pendingDiscontinuity = discontinuity;
}
public boolean readDiscontinuity() {
if (pendingDiscontinuity) {
extractor.reset();
pendingDiscontinuity = false;
return true;
}
return false;
}
public boolean prepare() {
return extractor.prepare(getNonBlockingInputStream());
}
public int getTrackCount() {
return extractor.getTrackCount();
}
public boolean sampleAvailable() {
// TODO: Maybe optimize this to not require looping over the tracks.
if (!prepare()) {
return false;
}
// TODO: Optimize this to not require looping over the tracks.
NonBlockingInputStream inputStream = getNonBlockingInputStream();
int trackCount = extractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
int result = extractor.read(inputStream, i, null);
if ((result & TsExtractor.RESULT_NEED_SAMPLE_HOLDER) != 0) {
return true;
}
}
return false;
}
public boolean read(int track, SampleHolder holder) {
int result = extractor.read(getNonBlockingInputStream(), track, holder);
return (result & TsExtractor.RESULT_READ_SAMPLE) != 0;
}
public void reset() {
extractor.reset();
pendingDiscontinuity = discontinuity;
resetReadPosition();
}
public MediaFormat getMediaFormat(int track) {
return extractor.getFormat(track);
}
public boolean isLastChunk() {
return nextChunkIndex == -1;
}
public void reset() {
resetReadPosition();
pendingDiscontinuity = discontinuity;
}
public boolean hasPendingDiscontinuity() {
return pendingDiscontinuity;
}
public void clearPendingDiscontinuity() {
pendingDiscontinuity = false;
}
}

View file

@ -37,19 +37,6 @@ import java.util.Queue;
*/
public final class TsExtractor {
/**
* An attempt to read from the input stream returned insufficient data.
*/
public static final int RESULT_NEED_MORE_DATA = 1;
/**
* A media sample was read.
*/
public static final int RESULT_READ_SAMPLE = 2;
/**
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
*/
public static final int RESULT_NEED_SAMPLE_HOLDER = 4;
private static final String TAG = "TsExtractor";
private static final int TS_PACKET_SIZE = 188;
@ -69,12 +56,18 @@ public final class TsExtractor {
private boolean prepared;
private boolean pendingTimestampOffsetUpdate;
private long pendingTimestampOffsetUs;
private long sampleTimestampOffsetUs;
private long largestParsedTimestampUs;
public TsExtractor() {
tsPacketBuffer = new BitsArray();
pesPayloadReaders = new SparseArray<PesPayloadReader>();
tsPayloadReaders = new SparseArray<TsPayloadReader>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
samplesPool = new LinkedList<Sample>();
largestParsedTimestampUs = Long.MIN_VALUE;
}
/**
@ -102,10 +95,19 @@ public final class TsExtractor {
return pesPayloadReaders.valueAt(track).getMediaFormat();
}
/**
* Whether the extractor is prepared.
*
* @return True if the extractor is prepared. False otherwise.
*/
public boolean isPrepared() {
return prepared;
}
/**
* Resets the extractor's internal state.
*/
public void reset() {
public void reset(long nextSampleTimestampUs) {
prepared = false;
tsPacketBuffer.reset();
tsPayloadReaders.clear();
@ -115,27 +117,67 @@ public final class TsExtractor {
pesPayloadReaders.valueAt(i).clear();
}
pesPayloadReaders.clear();
// Configure for subsequent read operations.
pendingTimestampOffsetUpdate = true;
pendingTimestampOffsetUs = nextSampleTimestampUs;
largestParsedTimestampUs = Long.MIN_VALUE;
}
/**
* Attempts to prepare the extractor. The extractor is prepared once it has read sufficient data
* to have established the available tracks and their corresponding media formats.
* Consumes data from a {@link NonBlockingInputStream}.
* <p>
* Calling this method is a no-op if the extractor is already prepared.
* The read terminates if the end of the input stream is reached, if insufficient data is
* available to read a sample, or if the extractor has consumed up to the specified target
* timestamp.
*
* @param inputStream The input stream from which data can be read.
* @return True if the extractor was prepared. False if more data is required.
* @param inputStream The input stream from which data should be read.
* @param targetTimestampUs A target timestamp to consume up to.
* @return True if the target timestamp was reached. False otherwise.
*/
public boolean prepare(NonBlockingInputStream inputStream) {
while (!prepared) {
if (readTSPacket(inputStream) == -1) {
return false;
}
public boolean consumeUntil(NonBlockingInputStream inputStream, long targetTimestampUs) {
while (largestParsedTimestampUs < targetTimestampUs && readTSPacket(inputStream) != -1) {
// Carry on.
}
if (!prepared) {
prepared = checkPrepared();
}
return largestParsedTimestampUs >= targetTimestampUs;
}
/**
* Gets the next sample for the specified track.
*
* @param track The track from which to read.
* @param out A {@link SampleHolder} into which the next sample should be read.
* @return True if a sample was read. False otherwise.
*/
public boolean getSample(int track, SampleHolder out) {
Assertions.checkState(prepared);
Queue<Sample> queue = pesPayloadReaders.valueAt(track).samplesQueue;
if (queue.isEmpty()) {
return false;
}
Sample sample = queue.remove();
convert(sample, out);
recycleSample(sample);
return true;
}
/**
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)}.
*
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}.
* False otherwise.
*/
public boolean hasSamples() {
for (int i = 0; i < pesPayloadReaders.size(); i++) {
if (!pesPayloadReaders.valueAt(i).samplesQueue.isEmpty()) {
return true;
}
}
return false;
}
private boolean checkPrepared() {
int pesPayloadReaderCount = pesPayloadReaders.size();
if (pesPayloadReaderCount == 0) {
@ -149,40 +191,6 @@ public final class TsExtractor {
return true;
}
/**
* Consumes data from a {@link NonBlockingInputStream}.
* <p>
* The read terminates if the end of the input stream is reached, if insufficient data is
* available to read a sample, or if a sample is read. The returned flags indicate
* both the reason for termination and data that was parsed during the read.
*
* @param inputStream The input stream from which data should be read.
* @param track The track from which to read.
* @param out A {@link SampleHolder} into which the next sample should be read. If null then
* {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached.
* @return One or more of the {@code RESULT_*} flags defined in this class.
*/
public int read(NonBlockingInputStream inputStream, int track, SampleHolder out) {
Assertions.checkState(prepared);
Queue<Sample> queue = pesPayloadReaders.valueAt(track).samplesQueue;
// Keep reading if the buffer is empty.
while (queue.isEmpty()) {
if (readTSPacket(inputStream) == -1) {
return RESULT_NEED_MORE_DATA;
}
}
if (!queue.isEmpty() && out == null) {
return RESULT_NEED_SAMPLE_HOLDER;
}
Sample sample = queue.remove();
convert(sample, out);
recycleSample(sample);
return RESULT_READ_SAMPLE;
}
/**
* Read a single TS packet.
*/
@ -506,6 +514,12 @@ public final class TsExtractor {
addToSample(sample, buffer, sampleSize);
sample.flags = flags;
sample.timeUs = sampleTimeUs;
addSample(sample);
}
protected void addSample(Sample sample) {
adjustTimestamp(sample);
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs);
samplesQueue.add(sample);
}
@ -517,6 +531,14 @@ public final class TsExtractor {
sample.size += size;
}
private void adjustTimestamp(Sample sample) {
if (pendingTimestampOffsetUpdate) {
sampleTimestampOffsetUs = pendingTimestampOffsetUs - sample.timeUs;
pendingTimestampOffsetUpdate = false;
}
sample.timeUs += sampleTimestampOffsetUs;
}
}
/**
@ -549,7 +571,7 @@ public final class TsExtractor {
// Single PES packet should contain only one new H.264 frame.
if (currentSample != null) {
samplesQueue.add(currentSample);
addSample(currentSample);
}
currentSample = getSample();
pesPayloadSize -= readOneH264Frame(pesBuffer, false);