Skip to content

Commit

Permalink
Refactor HLS support.
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
ojw28 committed Oct 28, 2014
1 parent d3a05c9 commit 2422912
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 202 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
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;
Expand All @@ -40,10 +39,10 @@
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;
Expand All @@ -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();
}

Expand Down Expand Up @@ -120,6 +118,7 @@ public void getChunkOperation(List<TsChunk> queue, long seekPositionUs, long pla
}
}
} else {
// Not live.
if (queue.isEmpty()) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true,
true) + mediaPlaylist.mediaSequence;
Expand Down Expand Up @@ -151,14 +150,26 @@ public void getChunkOperation(List<TsChunk> queue, long seekPositionUs, long pla

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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
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;
Expand All @@ -43,8 +45,10 @@
*/
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;
Expand Down Expand Up @@ -73,16 +77,14 @@ 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;
this.loadControl = loadControl;
this.bufferSizeContribution = bufferSizeContribution;
this.frameAccurateSeeking = frameAccurateSeeking;
this.remainingReleaseCount = downstreamRendererCount;
extractor = new TsExtractor();
currentLoadableHolder = new HlsChunkOperationHolder();
mediaChunks = new LinkedList<TsChunk>();
readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks);
Expand All @@ -93,32 +95,23 @@ public boolean prepare() throws IOException {
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;
}

Expand All @@ -142,9 +135,7 @@ public void enable(int track, long positionUs) {
trackEnabledStates[track] = true;
downstreamMediaFormats[track] = null;
if (enabledTrackCount == 1) {
downstreamPositionUs = positionUs;
lastSeekPositionUs = positionUs;
restartFrom(positionUs);
seekToUs(positionUs);
}
}

Expand All @@ -170,93 +161,87 @@ public boolean continueBuffering(long playbackPositionUs) throws IOException {
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();
}
}

@Override
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
Assertions.checkState(prepared);

if (pendingDiscontinuities[track]) {
pendingDiscontinuities[track] = false;
return DISCONTINUITY_READ;
TsChunk mediaChunk = mediaChunks.getFirst();
if (mediaChunk.isReadFinished() && mediaChunks.size() > 1) {
discardDownstreamHlsChunk();
mediaChunk = mediaChunks.getFirst();
}

if (onlyReadDiscontinuity) {
return NOTHING_READ;
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();
}
}

downstreamPositionUs = playbackPositionUs;
if (isPendingReset()) {
if (currentLoadableException != null) {
throw currentLoadableException;
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();
}
return NOTHING_READ;
}

TsChunk mediaChunk = mediaChunks.getFirst();
if (!haveSufficientSamples && currentLoadableException != null) {
throw currentLoadableException;
}
return haveSufficientSamples;
}

if (mediaChunk.readDiscontinuity()) {
pendingTimestampOffsetUpdate = true;
for (int i = 0; i < pendingDiscontinuities.length; i++) {
pendingDiscontinuities[i] = true;
}
@Override
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) {
Assertions.checkState(prepared);
downstreamPositionUs = playbackPositionUs;

if (pendingDiscontinuities[track]) {
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;
}
if (onlyReadDiscontinuity || isPendingReset() || !extractor.isPrepared()) {
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;
downstreamMediaFormats[track] = mediaFormat;
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
Expand All @@ -276,9 +261,9 @@ public void seekToUs(long positionUs) {
if (mediaChunk == null) {
restartFrom(positionUs);
} else {
pendingTimestampOffsetUpdate = true;
mediaChunk.reset();
discardDownstreamHlsChunks(mediaChunk);
mediaChunk.reset();
extractor.reset(mediaChunk.startTimeUs);
updateLoadControl();
}
}
Expand Down Expand Up @@ -503,12 +488,11 @@ private void maybeStartLoading() {
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);
}
Expand Down
Loading

0 comments on commit 2422912

Please sign in to comment.