HLS improvements + steps towards ABR.

This commit is contained in:
Oliver Woodman 2014-11-06 19:22:14 +00:00
parent 9790430a62
commit aeb17e6a88
4 changed files with 182 additions and 113 deletions

View file

@ -45,7 +45,6 @@ public class HlsChunkSource {
private final HlsMasterPlaylist masterPlaylist;
private final HlsMediaPlaylistParser mediaPlaylistParser;
private long liveStartTimeUs;
/* package */ HlsMediaPlaylist mediaPlaylist;
/* package */ boolean mediaPlaylistWasLive;
/* package */ long lastMediaPlaylistLoadTimeMs;
@ -168,25 +167,22 @@ public class HlsChunkSource {
DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null);
long startTimeUs = segment.startTimeUs;
long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000);
long startTimeUs;
int nextChunkMediaSequence = chunkMediaSequence + 1;
if (mediaPlaylistWasLive) {
if (queue.isEmpty()) {
liveStartTimeUs = startTimeUs;
startTimeUs = 0;
endTimeUs -= liveStartTimeUs;
} else {
startTimeUs -= liveStartTimeUs;
endTimeUs -= liveStartTimeUs;
startTimeUs = queue.get(queue.size() - 1).endTimeUs;
}
} else {
// Not live.
startTimeUs = segment.startTimeUs;
if (chunkIndex == mediaPlaylist.segments.size() - 1) {
nextChunkMediaSequence = -1;
}
}
long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000);
DataSource dataSource;
if (encryptedDataSource != null) {
@ -194,9 +190,8 @@ public class HlsChunkSource {
} else {
dataSource = upstreamDataSource;
}
out.chunk = new TsChunk(dataSource, dataSpec, 0, startTimeUs, endTimeUs,
nextChunkMediaSequence, segment.discontinuity);
out.chunk = new TsChunk(dataSource, dataSpec, 0, 0, startTimeUs, endTimeUs,
nextChunkMediaSequence, segment.discontinuity, false);
}
private boolean shouldRerequestMediaPlaylist() {

View file

@ -48,10 +48,11 @@ 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 TsExtractor.SamplePool samplePool;
private final LoadControl loadControl;
private final HlsChunkSource chunkSource;
private final HlsChunkOperationHolder currentLoadableHolder;
private final LinkedList<TsExtractor> extractors;
private final LinkedList<TsChunk> mediaChunks;
private final List<TsChunk> readOnlyHlsChunks;
private final int bufferSizeContribution;
@ -84,7 +85,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
this.bufferSizeContribution = bufferSizeContribution;
this.frameAccurateSeeking = frameAccurateSeeking;
this.remainingReleaseCount = downstreamRendererCount;
extractor = new TsExtractor();
samplePool = new TsExtractor.SamplePool();
extractors = new LinkedList<TsExtractor>();
currentLoadableHolder = new HlsChunkOperationHolder();
mediaChunks = new LinkedList<TsChunk>();
readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks);
@ -100,6 +102,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
loadControl.register(this, bufferSizeContribution);
}
continueBufferingInternal();
if (extractors.isEmpty()) {
return false;
}
TsExtractor extractor = extractors.get(0);
if (extractor.isPrepared()) {
trackCount = extractor.getTrackCount();
trackEnabledStates = new boolean[trackCount];
@ -171,39 +177,38 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
TsChunk mediaChunk = mediaChunks.getFirst();
int currentVariant = mediaChunk.variantIndex;
TsExtractor extractor;
if (extractors.isEmpty()) {
extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool);
extractors.addLast(extractor);
if (mediaChunk.discardFromFirstKeyframes) {
extractor.discardFromNextKeyframes();
}
} else {
extractor = extractors.getLast();
}
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);
mediaChunk.clearPendingDiscontinuity();
if (pendingDiscontinuities == null) {
// We're not prepared yet.
} else {
for (int i = 0; i < pendingDiscontinuities.length; i++) {
pendingDiscontinuities[i] = true;
}
}
if (mediaChunk.discontinuity || mediaChunk.variantIndex != currentVariant) {
extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool);
extractors.addLast(extractor);
}
if (mediaChunk.discardFromFirstKeyframes) {
extractor.discardFromNextKeyframes();
}
}
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);
// Allow the extractor to consume from the current chunk.
NonBlockingInputStream inputStream = mediaChunk.getNonBlockingInputStream();
boolean haveSufficientSamples = extractor.consumeUntil(inputStream,
downstreamPositionUs + MAX_SAMPLE_INTERLEAVING_OFFSET_US);
if (!haveSufficientSamples) {
// If we can't read any more, then we always say we have sufficient samples.
if (!haveSufficientSamples) {
haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished();
}
haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished();
}
if (!haveSufficientSamples && currentLoadableException != null) {
@ -223,7 +228,28 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return DISCONTINUITY_READ;
}
if (onlyReadDiscontinuity || isPendingReset() || !extractor.isPrepared()) {
if (onlyReadDiscontinuity || isPendingReset()) {
return NOTHING_READ;
}
if (extractors.isEmpty()) {
return NOTHING_READ;
}
TsExtractor extractor = extractors.getFirst();
while (extractors.size() > 1 && !extractor.hasSamples()) {
// We're finished reading from the extractor for all tracks, and so can discard it.
extractors.removeFirst().clear();
extractor = extractors.getFirst();
}
int extractorIndex = 0;
while (extractors.size() > extractorIndex + 1 && !extractor.hasSamples(track)) {
// We're finished reading from the extractor for this particular track, so advance to the
// next one for the current read.
extractor = extractors.get(++extractorIndex);
}
if (!extractor.isPrepared()) {
return NOTHING_READ;
}
@ -265,9 +291,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
if (mediaChunk == null) {
restartFrom(positionUs);
} else {
discardExtractors();
discardDownstreamHlsChunks(mediaChunk);
mediaChunk.reset();
extractor.reset(mediaChunk.startTimeUs);
mediaChunk.resetReadPosition();
updateLoadControl();
}
}
@ -494,13 +520,20 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
TsChunk mediaChunk = (TsChunk) currentLoadable;
mediaChunks.add(mediaChunk);
if (isPendingReset()) {
extractor.reset(mediaChunk.startTimeUs);
discardExtractors();
pendingResetPositionUs = NO_RESET_PENDING;
}
}
loader.startLoading(currentLoadable, this);
}
private void discardExtractors() {
for (int i = 0; i < extractors.size(); i++) {
extractors.get(i).clear();
}
extractors.clear();
}
/**
* Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not
* itself discarded. Null can be passed to discard all media chunks.

View file

@ -23,6 +23,10 @@ import com.google.android.exoplayer.upstream.DataSpec;
*/
public final class TsChunk extends HlsChunk {
/**
* The index of the variant in the master playlist.
*/
public final int variantIndex;
/**
* The start time of the media contained by the chunk.
*/
@ -38,44 +42,38 @@ public final class TsChunk extends HlsChunk {
/**
* The encoding discontinuity indicator.
*/
private final boolean discontinuity;
private boolean pendingDiscontinuity;
public final boolean discontinuity;
/**
* For each track, whether samples from the first keyframe (inclusive) should be discarded.
*/
public final boolean discardFromFirstKeyframes;
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param trigger The reason for this chunk being selected.
* @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 discontinuity The encoding discontinuity indicator.
* @param discardFromFirstKeyframes For each contained media stream, whether samples from the
* first keyframe (inclusive) should be discarded.
*/
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, long startTimeUs,
long endTimeUs, int nextChunkIndex, boolean discontinuity) {
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, int variantIndex,
long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity,
boolean discardFromFirstKeyframes) {
super(dataSource, dataSpec, trigger);
this.variantIndex = variantIndex;
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
this.nextChunkIndex = nextChunkIndex;
this.discontinuity = discontinuity;
this.pendingDiscontinuity = discontinuity;
this.discardFromFirstKeyframes = discardFromFirstKeyframes;
}
public boolean isLastChunk() {
return nextChunkIndex == -1;
}
public void reset() {
resetReadPosition();
pendingDiscontinuity = discontinuity;
}
public boolean hasPendingDiscontinuity() {
return pendingDiscontinuity;
}
public void clearPendingDiscontinuity() {
pendingDiscontinuity = false;
}
}

View file

@ -23,11 +23,13 @@ import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.SuppressLint;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Queue;
@ -47,26 +49,27 @@ public final class TsExtractor {
private static final int TS_STREAM_TYPE_H264 = 0x1B;
private static final int TS_STREAM_TYPE_ID3 = 0x15;
private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;
private final BitsArray tsPacketBuffer;
private final SparseArray<PesPayloadReader> pesPayloadReaders; // Indexed by streamType
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final Queue<Sample> samplesPool;
private final SamplePool samplePool;
private boolean prepared;
/* package */ boolean pendingTimestampOffsetUpdate;
/* package */ long pendingTimestampOffsetUs;
/* package */ boolean pendingFirstSampleTimestampAdjustment;
/* package */ long firstSampleTimestamp;
/* package */ long sampleTimestampOffsetUs;
/* package */ long largestParsedTimestampUs;
/* package */ boolean discardFromNextKeyframes;
public TsExtractor() {
public TsExtractor(long firstSampleTimestamp, SamplePool samplePool) {
this.firstSampleTimestamp = firstSampleTimestamp;
this.samplePool = samplePool;
pendingFirstSampleTimestampAdjustment = true;
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;
}
@ -105,22 +108,19 @@ public final class TsExtractor {
}
/**
* Resets the extractor's internal state.
* Flushes any pending or incomplete samples, returning them to the sample pool.
*/
public void reset(long nextSampleTimestampUs) {
prepared = false;
tsPacketBuffer.reset();
tsPayloadReaders.clear();
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
// Clear each reader before discarding it, so as to recycle any queued Sample objects.
public void clear() {
for (int i = 0; i < pesPayloadReaders.size(); i++) {
pesPayloadReaders.valueAt(i).clear();
}
pesPayloadReaders.clear();
// Configure for subsequent read operations.
pendingTimestampOffsetUpdate = true;
pendingTimestampOffsetUs = nextSampleTimestampUs;
largestParsedTimestampUs = Long.MIN_VALUE;
}
/**
* For each track, whether to discard samples from the next keyframe (inclusive).
*/
public void discardFromNextKeyframes() {
discardFromNextKeyframes = true;
}
/**
@ -153,31 +153,43 @@ public final class TsExtractor {
*/
public boolean getSample(int track, SampleHolder out) {
Assertions.checkState(prepared);
Queue<Sample> queue = pesPayloadReaders.valueAt(track).samplesQueue;
Queue<Sample> queue = pesPayloadReaders.valueAt(track).sampleQueue;
if (queue.isEmpty()) {
return false;
}
Sample sample = queue.remove();
convert(sample, out);
recycleSample(sample);
samplePool.recycle(sample);
return true;
}
/**
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)}.
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for any
* track.
*
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}.
* False otherwise.
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
* for any track. False otherwise.
*/
public boolean hasSamples() {
for (int i = 0; i < pesPayloadReaders.size(); i++) {
if (!pesPayloadReaders.valueAt(i).samplesQueue.isEmpty()) {
if (hasSamples(i)) {
return true;
}
}
return false;
}
/**
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the
* specified track.
*
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
* for the specified track. False otherwise.
*/
public boolean hasSamples(int track) {
return !pesPayloadReaders.valueAt(track).sampleQueue.isEmpty();
}
private boolean checkPrepared() {
int pesPayloadReaderCount = pesPayloadReaders.size();
if (pesPayloadReaderCount == 0) {
@ -251,18 +263,6 @@ public final class TsExtractor {
out.timeUs = in.timeUs;
}
/* package */ Sample getSample() {
if (samplesPool.isEmpty()) {
return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE);
}
return samplesPool.remove();
}
/* package */ void recycleSample(Sample sample) {
sample.reset();
samplesPool.add(sample);
}
/**
* Parses payload data.
*/
@ -484,12 +484,14 @@ public final class TsExtractor {
*/
private abstract class PesPayloadReader {
public final Queue<Sample> samplesQueue;
public final Queue<Sample> sampleQueue;
private MediaFormat mediaFormat;
private boolean foundFirstKeyframe;
private boolean foundLastKeyframe;
protected PesPayloadReader() {
this.samplesQueue = new LinkedList<Sample>();
this.sampleQueue = new LinkedList<Sample>();
}
public boolean hasMediaFormat() {
@ -507,8 +509,8 @@ public final class TsExtractor {
public abstract void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs);
public void clear() {
while (!samplesQueue.isEmpty()) {
recycleSample(samplesQueue.remove());
while (!sampleQueue.isEmpty()) {
samplePool.recycle(sampleQueue.remove());
}
}
@ -520,17 +522,31 @@ public final class TsExtractor {
* @param sampleTimeUs The sample time stamp.
*/
protected void addSample(BitsArray buffer, int sampleSize, long sampleTimeUs, int flags) {
Sample sample = getSample();
Sample sample = samplePool.get();
addToSample(sample, buffer, sampleSize);
sample.flags = flags;
sample.timeUs = sampleTimeUs;
addSample(sample);
}
@SuppressLint("InlinedApi")
protected void addSample(Sample sample) {
boolean isKeyframe = (sample.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
if (isKeyframe) {
if (!foundFirstKeyframe) {
foundFirstKeyframe = true;
}
if (discardFromNextKeyframes) {
foundLastKeyframe = true;
}
}
adjustTimestamp(sample);
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs);
samplesQueue.add(sample);
if (foundFirstKeyframe && !foundLastKeyframe) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs);
sampleQueue.add(sample);
} else {
samplePool.recycle(sample);
}
}
protected void addToSample(Sample sample, BitsArray buffer, int size) {
@ -542,9 +558,9 @@ public final class TsExtractor {
}
private void adjustTimestamp(Sample sample) {
if (pendingTimestampOffsetUpdate) {
sampleTimestampOffsetUs = pendingTimestampOffsetUs - sample.timeUs;
pendingTimestampOffsetUpdate = false;
if (pendingFirstSampleTimestampAdjustment) {
sampleTimestampOffsetUs = firstSampleTimestamp - sample.timeUs;
pendingFirstSampleTimestampAdjustment = false;
}
sample.timeUs += sampleTimestampOffsetUs;
}
@ -583,7 +599,7 @@ public final class TsExtractor {
if (currentSample != null) {
addSample(currentSample);
}
currentSample = getSample();
currentSample = samplePool.get();
pesPayloadSize -= readOneH264Frame(pesBuffer, false);
currentSample.timeUs = pesTimeUs;
@ -615,7 +631,7 @@ public final class TsExtractor {
public void clear() {
super.clear();
if (currentSample != null) {
recycleSample(currentSample);
samplePool.recycle(currentSample);
currentSample = null;
}
}
@ -742,8 +758,35 @@ public final class TsExtractor {
}
/**
* Simplified version of SampleHolder for internal buffering.
*/
* A pool from which the extractor can obtain sample objects for internal use.
*/
public static class SamplePool {
private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;
private final ArrayList<Sample> samples;
public SamplePool() {
samples = new ArrayList<Sample>();
}
/* package */ Sample get() {
if (samples.isEmpty()) {
return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE);
}
return samples.remove(samples.size() - 1);
}
/* package */ void recycle(Sample sample) {
sample.reset();
samples.add(sample);
}
}
/**
* Simplified version of SampleHolder for internal buffering.
*/
private static class Sample {
public byte[] data;