Commit 8e2801ce by Oliver Woodman

Improve HLS ABR.

- Add options to switch abruptly at segment boundaries. Third
  parties who guarantee keyframes at the start of segments will
  want this, because it makes switching more efficient and hence
  rebuffering less likely.
- Switch quality faster when performing a splicing switch (when
  we detect that we need to switch variant, we now immediately
  request the same segment as we did last time for the new variant,
  rather than requesting one more segment for the old variant
  before doing this.
parent 410fcdeb
......@@ -80,8 +80,9 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter);
boolean adaptiveDecoder = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false).adaptive;
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, url, manifest, bandwidthMeter, null,
MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false).adaptive);
adaptiveDecoder ? HlsChunkSource.ADAPTIVE_MODE_SPLICE : HlsChunkSource.ADAPTIVE_MODE_NONE);
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 3);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, player.getMainHandler(), player, 50);
......
......@@ -77,8 +77,9 @@ import java.io.IOException;
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter);
boolean adaptiveDecoder = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false).adaptive;
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, url, manifest, bandwidthMeter, null,
MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false).adaptive);
adaptiveDecoder ? HlsChunkSource.ADAPTIVE_MODE_SPLICE : HlsChunkSource.ADAPTIVE_MODE_NONE);
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 2);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, playerActivity.getMainHandler(),
......
......@@ -35,12 +35,18 @@ import java.util.LinkedList;
*/
public class HlsSampleSource implements SampleSource, Loader.Callback {
/**
* The default minimum number of times to retry loading data prior to failing.
*/
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 1;
private static final long BUFFER_DURATION_US = 20000000;
private static final int NO_RESET_PENDING = -1;
private final HlsChunkSource chunkSource;
private final LinkedList<TsExtractor> extractors;
private final boolean frameAccurateSeeking;
private final int minLoadableRetryCount;
private int remainingReleaseCount;
private boolean prepared;
......@@ -65,11 +71,18 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
public HlsSampleSource(HlsChunkSource chunkSource,
boolean frameAccurateSeeking, int downstreamRendererCount) {
public HlsSampleSource(HlsChunkSource chunkSource, boolean frameAccurateSeeking,
int downstreamRendererCount) {
this(chunkSource, frameAccurateSeeking, downstreamRendererCount,
DEFAULT_MIN_LOADABLE_RETRY_COUNT);
}
public HlsSampleSource(HlsChunkSource chunkSource, boolean frameAccurateSeeking,
int downstreamRendererCount, int minLoadableRetryCount) {
this.chunkSource = chunkSource;
this.frameAccurateSeeking = frameAccurateSeeking;
this.remainingReleaseCount = downstreamRendererCount;
this.minLoadableRetryCount = minLoadableRetryCount;
extractors = new LinkedList<TsExtractor>();
}
......@@ -97,8 +110,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
prepared = true;
}
}
if (!prepared && currentLoadableException != null) {
throw currentLoadableException;
if (!prepared) {
maybeThrowLoadableException();
}
return prepared;
}
......@@ -157,8 +170,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return false;
}
boolean haveSamples = extractors.getFirst().hasSamples();
if (!haveSamples && currentLoadableException != null) {
throw currentLoadableException;
if (!haveSamples) {
maybeThrowLoadableException();
}
return haveSamples;
}
......@@ -175,9 +188,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) {
if (currentLoadableException != null) {
throw currentLoadableException;
}
maybeThrowLoadableException();
return NOTHING_READ;
}
......@@ -202,9 +213,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
if (!extractor.isPrepared()) {
if (currentLoadableException != null) {
throw currentLoadableException;
}
maybeThrowLoadableException();
return NOTHING_READ;
}
......@@ -225,9 +234,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return END_OF_STREAM;
}
if (currentLoadableException != null) {
throw currentLoadableException;
}
maybeThrowLoadableException();
return NOTHING_READ;
}
......@@ -283,7 +290,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} finally {
if (isTsChunk(currentLoadable)) {
TsChunk tsChunk = (TsChunk) loadable;
loadingFinished = tsChunk.isLastChunk();
loadingFinished = tsChunk.isLastChunk;
}
if (!currentLoadableExceptionFatal) {
clearCurrentLoadable();
......@@ -309,6 +316,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
maybeStartLoading();
}
private void maybeThrowLoadableException() throws IOException {
if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) {
throw currentLoadableException;
}
}
private void restartFrom(long positionUs) {
pendingResetPositionUs = positionUs;
loadingFinished = false;
......
......@@ -39,14 +39,13 @@ public final class TsChunk extends HlsChunk {
*/
public final long endTimeUs;
/**
* The index of the next media chunk, or -1 if this is the last media chunk in the stream.
* The chunk index.
*/
public final int nextChunkIndex;
public final int chunkIndex;
/**
* True if this is the final chunk being loaded for the current variant, as we splice to another
* one. False otherwise.
* True if this is the last chunk in the media. False otherwise.
*/
public final boolean splicingOut;
public final boolean isLastChunk;
/**
* The extractor into which this chunk is being consumed.
*/
......@@ -62,19 +61,18 @@ public final class TsChunk extends HlsChunk {
* @param variantIndex The index of the variant in the master playlist.
* @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 splicingOut True if this is the final chunk being loaded for the current variant, as we
* splice to another one. False otherwise.
* @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise.
*/
public TsChunk(DataSource dataSource, DataSpec dataSpec, TsExtractor tsExtractor,
int variantIndex, long startTimeUs, long endTimeUs, int nextChunkIndex, boolean splicingOut) {
int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
super(dataSource, dataSpec);
this.extractor = tsExtractor;
this.variantIndex = variantIndex;
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
this.nextChunkIndex = nextChunkIndex;
this.splicingOut = splicingOut;
this.chunkIndex = chunkIndex;
this.isLastChunk = isLastChunk;
}
@Override
......@@ -82,10 +80,6 @@ public final class TsChunk extends HlsChunk {
// Do nothing.
}
public boolean isLastChunk() {
return nextChunkIndex == -1;
}
@Override
public boolean isLoadFinished() {
return loadFinished;
......
......@@ -56,6 +56,7 @@ public final class TsExtractor {
private final SparseArray<SampleQueue> sampleQueues; // Indexed by streamType
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final SamplePool samplePool;
private final boolean shouldSpliceIn;
/* package */ final long firstSampleTimestamp;
// Accessed only by the consuming thread.
......@@ -69,9 +70,10 @@ public final class TsExtractor {
private volatile boolean prepared;
/* package */ volatile long largestParsedTimestampUs;
public TsExtractor(long firstSampleTimestamp, SamplePool samplePool) {
public TsExtractor(long firstSampleTimestamp, SamplePool samplePool, boolean shouldSpliceIn) {
this.firstSampleTimestamp = firstSampleTimestamp;
this.samplePool = samplePool;
this.shouldSpliceIn = shouldSpliceIn;
pendingFirstSampleTimestampAdjustment = true;
tsPacketBuffer = new BitArray();
sampleQueues = new SparseArray<SampleQueue>();
......@@ -141,9 +143,9 @@ public final class TsExtractor {
*/
public void configureSpliceTo(TsExtractor nextExtractor) {
Assertions.checkState(prepared);
if (spliceConfigured || !nextExtractor.isPrepared()) {
// The splice is already configured or the next extractor isn't ready to be spliced in.
// Already configured, or too early to splice.
if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) {
// The splice is already configured, or the next extractor doesn't want to be spliced in, or
// the next extractor isn't ready to be spliced in.
return;
}
boolean spliceConfigured = true;
......
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