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;
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() {
......
......@@ -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 boolean isLastChunk() {
return nextChunkIndex == -1;
}
public void reset() {
extractor.reset();
pendingDiscontinuity = discontinuity;
resetReadPosition();
pendingDiscontinuity = discontinuity;
}
public MediaFormat getMediaFormat(int track) {
return extractor.getFormat(track);
public boolean hasPendingDiscontinuity() {
return pendingDiscontinuity;
}
public boolean isLastChunk() {
return nextChunkIndex == -1;
public void clearPendingDiscontinuity() {
pendingDiscontinuity = false;
}
}
......@@ -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;
}
/**
......@@ -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.
*/
public void reset() {
public void reset(long nextSampleTimestampUs) {
prepared = false;
tsPacketBuffer.reset();
tsPayloadReaders.clear();
......@@ -115,72 +117,78 @@ 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 true;
return largestParsedTimestampUs >= targetTimestampUs;
}
private boolean checkPrepared() {
int pesPayloadReaderCount = pesPayloadReaders.size();
if (pesPayloadReaderCount == 0) {
/**
* 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;
}
for (int i = 0; i < pesPayloadReaderCount; i++) {
if (!pesPayloadReaders.valueAt(i).hasMediaFormat()) {
return false;
}
}
Sample sample = queue.remove();
convert(sample, out);
recycleSample(sample);
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.
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)}.
*
* @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.
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}.
* False otherwise.
*/
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;
public boolean hasSamples() {
for (int i = 0; i < pesPayloadReaders.size(); i++) {
if (!pesPayloadReaders.valueAt(i).samplesQueue.isEmpty()) {
return true;
}
}
return false;
}
if (!queue.isEmpty() && out == null) {
return RESULT_NEED_SAMPLE_HOLDER;
private boolean checkPrepared() {
int pesPayloadReaderCount = pesPayloadReaders.size();
if (pesPayloadReaderCount == 0) {
return false;
}
Sample sample = queue.remove();
convert(sample, out);
recycleSample(sample);
return RESULT_READ_SAMPLE;
for (int i = 0; i < pesPayloadReaderCount; i++) {
if (!pesPayloadReaders.valueAt(i).hasMediaFormat()) {
return false;
}
}
return true;
}
/**
......@@ -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);
......
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