Commit 2422912b by Oliver Woodman

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
parent d3a05c9a
...@@ -18,7 +18,6 @@ package com.google.android.exoplayer.hls; ...@@ -18,7 +18,6 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TrackRenderer; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
...@@ -40,10 +39,10 @@ import java.util.List; ...@@ -40,10 +39,10 @@ import java.util.List;
public class HlsChunkSource { public class HlsChunkSource {
private final DataSource dataSource; private final DataSource dataSource;
private final TsExtractor extractor;
private final HlsMasterPlaylist masterPlaylist; private final HlsMasterPlaylist masterPlaylist;
private final HlsMediaPlaylistParser mediaPlaylistParser; private final HlsMediaPlaylistParser mediaPlaylistParser;
private long liveStartTimeUs;
/* package */ HlsMediaPlaylist mediaPlaylist; /* package */ HlsMediaPlaylist mediaPlaylist;
/* package */ boolean mediaPlaylistWasLive; /* package */ boolean mediaPlaylistWasLive;
/* package */ long lastMediaPlaylistLoadTimeMs; /* package */ long lastMediaPlaylistLoadTimeMs;
...@@ -52,7 +51,6 @@ public class HlsChunkSource { ...@@ -52,7 +51,6 @@ public class HlsChunkSource {
public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist) { public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist) {
this.dataSource = dataSource; this.dataSource = dataSource;
this.masterPlaylist = masterPlaylist; this.masterPlaylist = masterPlaylist;
extractor = new TsExtractor();
mediaPlaylistParser = new HlsMediaPlaylistParser(); mediaPlaylistParser = new HlsMediaPlaylistParser();
} }
...@@ -120,6 +118,7 @@ public class HlsChunkSource { ...@@ -120,6 +118,7 @@ public class HlsChunkSource {
} }
} }
} else { } else {
// Not live.
if (queue.isEmpty()) { if (queue.isEmpty()) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true, chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true,
true) + mediaPlaylist.mediaSequence; true) + mediaPlaylist.mediaSequence;
...@@ -151,14 +150,26 @@ public class HlsChunkSource { ...@@ -151,14 +150,26 @@ public class HlsChunkSource {
long startTimeUs = segment.startTimeUs; long startTimeUs = segment.startTimeUs;
long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000); long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000);
int nextChunkMediaSequence = chunkMediaSequence + 1; 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, out.chunk = new TsChunk(dataSource, dataSpec, 0, startTimeUs, endTimeUs, nextChunkMediaSequence,
nextChunkMediaSequence, segment.discontinuity); segment.discontinuity);
} }
private boolean shouldRerequestMediaPlaylist() { private boolean shouldRerequestMediaPlaylist() {
......
...@@ -15,12 +15,8 @@ ...@@ -15,12 +15,8 @@
*/ */
package com.google.android.exoplayer.hls; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
/** /**
* A MPEG2TS chunk. * A MPEG2TS chunk.
...@@ -44,82 +40,42 @@ public final class TsChunk extends HlsChunk { ...@@ -44,82 +40,42 @@ public final class TsChunk extends HlsChunk {
*/ */
private final boolean discontinuity; private final boolean discontinuity;
private final TsExtractor extractor;
private boolean pendingDiscontinuity; private boolean pendingDiscontinuity;
/** /**
* @param dataSource A {@link DataSource} for loading the data. * @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. * @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 trigger The reason for this chunk being selected.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @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 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 nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
* @param discontinuity The encoding discontinuity indicator. * @param discontinuity The encoding discontinuity indicator.
*/ */
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, TsExtractor extractor, public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, long startTimeUs,
long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity) { long endTimeUs, int nextChunkIndex, boolean discontinuity) {
super(dataSource, dataSpec, trigger); super(dataSource, dataSpec, trigger);
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs; this.endTimeUs = endTimeUs;
this.nextChunkIndex = nextChunkIndex; this.nextChunkIndex = nextChunkIndex;
this.extractor = extractor;
this.discontinuity = discontinuity; this.discontinuity = discontinuity;
this.pendingDiscontinuity = discontinuity; this.pendingDiscontinuity = discontinuity;
} }
public boolean readDiscontinuity() { public boolean isLastChunk() {
if (pendingDiscontinuity) { return nextChunkIndex == -1;
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() { public void reset() {
extractor.reset();
pendingDiscontinuity = discontinuity;
resetReadPosition(); resetReadPosition();
pendingDiscontinuity = discontinuity;
} }
public MediaFormat getMediaFormat(int track) { public boolean hasPendingDiscontinuity() {
return extractor.getFormat(track); return pendingDiscontinuity;
} }
public boolean isLastChunk() { public void clearPendingDiscontinuity() {
return nextChunkIndex == -1; pendingDiscontinuity = false;
} }
} }
...@@ -37,19 +37,6 @@ import java.util.Queue; ...@@ -37,19 +37,6 @@ import java.util.Queue;
*/ */
public final class TsExtractor { 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 String TAG = "TsExtractor";
private static final int TS_PACKET_SIZE = 188; private static final int TS_PACKET_SIZE = 188;
...@@ -69,12 +56,18 @@ public final class TsExtractor { ...@@ -69,12 +56,18 @@ public final class TsExtractor {
private boolean prepared; private boolean prepared;
private boolean pendingTimestampOffsetUpdate;
private long pendingTimestampOffsetUs;
private long sampleTimestampOffsetUs;
private long largestParsedTimestampUs;
public TsExtractor() { public TsExtractor() {
tsPacketBuffer = new BitsArray(); tsPacketBuffer = new BitsArray();
pesPayloadReaders = new SparseArray<PesPayloadReader>(); pesPayloadReaders = new SparseArray<PesPayloadReader>();
tsPayloadReaders = new SparseArray<TsPayloadReader>(); tsPayloadReaders = new SparseArray<TsPayloadReader>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader()); tsPayloadReaders.put(TS_PAT_PID, new PatReader());
samplesPool = new LinkedList<Sample>(); samplesPool = new LinkedList<Sample>();
largestParsedTimestampUs = Long.MIN_VALUE;
} }
/** /**
...@@ -103,9 +96,18 @@ public final class TsExtractor { ...@@ -103,9 +96,18 @@ public final class TsExtractor {
} }
/** /**
* 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. * Resets the extractor's internal state.
*/ */
public void reset() { public void reset(long nextSampleTimestampUs) {
prepared = false; prepared = false;
tsPacketBuffer.reset(); tsPacketBuffer.reset();
tsPayloadReaders.clear(); tsPayloadReaders.clear();
...@@ -115,72 +117,78 @@ public final class TsExtractor { ...@@ -115,72 +117,78 @@ public final class TsExtractor {
pesPayloadReaders.valueAt(i).clear(); pesPayloadReaders.valueAt(i).clear();
} }
pesPayloadReaders.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 * Consumes data from a {@link NonBlockingInputStream}.
* to have established the available tracks and their corresponding media formats.
* <p> * <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. * @param inputStream The input stream from which data should be read.
* @return True if the extractor was prepared. False if more data is required. * @param targetTimestampUs A target timestamp to consume up to.
* @return True if the target timestamp was reached. False otherwise.
*/ */
public boolean prepare(NonBlockingInputStream inputStream) { public boolean consumeUntil(NonBlockingInputStream inputStream, long targetTimestampUs) {
while (!prepared) { while (largestParsedTimestampUs < targetTimestampUs && readTSPacket(inputStream) != -1) {
if (readTSPacket(inputStream) == -1) { // Carry on.
return false; }
} if (!prepared) {
prepared = checkPrepared(); prepared = checkPrepared();
} }
return true; return largestParsedTimestampUs >= targetTimestampUs;
} }
private boolean checkPrepared() { /**
int pesPayloadReaderCount = pesPayloadReaders.size(); * Gets the next sample for the specified track.
if (pesPayloadReaderCount == 0) { *
* @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; return false;
} }
for (int i = 0; i < pesPayloadReaderCount; i++) { Sample sample = queue.remove();
if (!pesPayloadReaders.valueAt(i).hasMediaFormat()) { convert(sample, out);
return false; recycleSample(sample);
}
}
return true; return true;
} }
/** /**
* Consumes data from a {@link NonBlockingInputStream}. * Whether samples are available for reading from {@link #getSample(int, SampleHolder)}.
* <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. * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}.
* @param track The track from which to read. * False otherwise.
* @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) { public boolean hasSamples() {
Assertions.checkState(prepared); for (int i = 0; i < pesPayloadReaders.size(); i++) {
Queue<Sample> queue = pesPayloadReaders.valueAt(track).samplesQueue; if (!pesPayloadReaders.valueAt(i).samplesQueue.isEmpty()) {
return true;
// Keep reading if the buffer is empty.
while (queue.isEmpty()) {
if (readTSPacket(inputStream) == -1) {
return RESULT_NEED_MORE_DATA;
} }
} }
return false;
}
if (!queue.isEmpty() && out == null) { private boolean checkPrepared() {
return RESULT_NEED_SAMPLE_HOLDER; int pesPayloadReaderCount = pesPayloadReaders.size();
if (pesPayloadReaderCount == 0) {
return false;
} }
for (int i = 0; i < pesPayloadReaderCount; i++) {
Sample sample = queue.remove(); if (!pesPayloadReaders.valueAt(i).hasMediaFormat()) {
convert(sample, out); return false;
recycleSample(sample); }
return RESULT_READ_SAMPLE; }
return true;
} }
/** /**
...@@ -506,6 +514,12 @@ public final class TsExtractor { ...@@ -506,6 +514,12 @@ public final class TsExtractor {
addToSample(sample, buffer, sampleSize); addToSample(sample, buffer, sampleSize);
sample.flags = flags; sample.flags = flags;
sample.timeUs = sampleTimeUs; sample.timeUs = sampleTimeUs;
addSample(sample);
}
protected void addSample(Sample sample) {
adjustTimestamp(sample);
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs);
samplesQueue.add(sample); samplesQueue.add(sample);
} }
...@@ -517,6 +531,14 @@ public final class TsExtractor { ...@@ -517,6 +531,14 @@ public final class TsExtractor {
sample.size += size; 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 { ...@@ -549,7 +571,7 @@ public final class TsExtractor {
// Single PES packet should contain only one new H.264 frame. // Single PES packet should contain only one new H.264 frame.
if (currentSample != null) { if (currentSample != null) {
samplesQueue.add(currentSample); addSample(currentSample);
} }
currentSample = getSample(); currentSample = getSample();
pesPayloadSize -= readOneH264Frame(pesBuffer, false); pesPayloadSize -= readOneH264Frame(pesBuffer, false);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment