Move HLS to use a single RollingSampleBuffer per track.

Notes:
- RollingSampleBuffer will be renamed DefaultTrackOutput in a
  following CL, and variable naming will be sanitized.
- TsChunk will also be renamed to HlsMediaChunk, since it can
  be used for non-TS containers (e.g. MP3).
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=120240243
This commit is contained in:
olly 2016-04-19 10:04:53 -07:00 committed by Oliver Woodman
parent a7d7859478
commit b5bdbedfd5
7 changed files with 288 additions and 641 deletions

View File

@ -1,193 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.DecoderInputBuffer;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.upstream.Allocator;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* A {@link TrackOutput} that buffers extracted samples in a queue, and allows for consumption from
* that queue.
*/
public final class DefaultTrackOutput implements TrackOutput {
private final RollingSampleBuffer rollingBuffer;
private final DecoderInputBuffer sampleBuffer;
// Accessed only by the consuming thread.
private boolean needKeyframe;
private long lastReadTimeUs;
private long spliceOutTimeUs;
/**
* @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
*/
public DefaultTrackOutput(Allocator allocator) {
rollingBuffer = new RollingSampleBuffer(allocator);
sampleBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
needKeyframe = true;
lastReadTimeUs = Long.MIN_VALUE;
spliceOutTimeUs = Long.MIN_VALUE;
}
// Called by the consuming thread, but only when there is no loading thread.
/**
* Clears the queue, returning all allocations to the allocator.
*/
public void clear() {
rollingBuffer.clear();
needKeyframe = true;
lastReadTimeUs = Long.MIN_VALUE;
spliceOutTimeUs = Long.MIN_VALUE;
}
// Called by the consuming thread.
/**
* Returns the current upstream {@link Format}.
*/
public Format getUpstreamFormat() {
return rollingBuffer.getUpstreamFormat();
}
/**
* The largest timestamp of any sample received by the output, or {@link Long#MIN_VALUE} if a
* sample has yet to be received.
*/
public long getLargestParsedTimestampUs() {
return rollingBuffer.getLargestQueuedTimestampUs();
}
/**
* True if at least one sample can be read from the queue. False otherwise.
*/
public boolean isEmpty() {
return !advanceToEligibleSample();
}
/**
* Removes the next sample from the head of the queue, writing it into the provided buffer.
* <p>
* The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples
* queued prior to the first keyframe are discarded.
*
* @param buffer A {@link DecoderInputBuffer} into which the sample should be read.
* @return True if a sample was read. False otherwise.
*/
public boolean getSample(DecoderInputBuffer buffer) {
boolean foundEligibleSample = advanceToEligibleSample();
if (!foundEligibleSample) {
return false;
}
// Write the sample into the buffer.
rollingBuffer.readSample(buffer);
needKeyframe = false;
lastReadTimeUs = buffer.timeUs;
return true;
}
/**
* Skips all currently buffered samples.
*/
public void skipAllSamples() {
rollingBuffer.skipAllSamples();
}
/**
* Attempts to configure a splice from this queue to the next.
*
* @param nextQueue The queue being spliced to.
* @return Whether the splice was configured successfully.
*/
public boolean configureSpliceTo(DefaultTrackOutput nextQueue) {
if (spliceOutTimeUs != Long.MIN_VALUE) {
// We've already configured the splice.
return true;
}
long firstPossibleSpliceTime;
if (rollingBuffer.peekSample(sampleBuffer)) {
firstPossibleSpliceTime = sampleBuffer.timeUs;
} else {
firstPossibleSpliceTime = lastReadTimeUs + 1;
}
RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer;
while (nextRollingBuffer.peekSample(sampleBuffer)
&& (sampleBuffer.timeUs < firstPossibleSpliceTime || !sampleBuffer.isKeyFrame())) {
// Discard samples from the next queue for as long as they are before the earliest possible
// splice time, or not keyframes.
nextRollingBuffer.skipSample();
}
if (nextRollingBuffer.peekSample(sampleBuffer)) {
// We've found a keyframe in the next queue that can serve as the splice point. Set the
// splice point now.
spliceOutTimeUs = sampleBuffer.timeUs;
return true;
}
return false;
}
/**
* Advances the underlying buffer to the next sample that is eligible to be returned.
*
* @return True if an eligible sample was found. False otherwise, in which case the underlying
* buffer has been emptied.
*/
private boolean advanceToEligibleSample() {
boolean haveNext = rollingBuffer.peekSample(sampleBuffer);
if (needKeyframe) {
while (haveNext && !sampleBuffer.isKeyFrame()) {
rollingBuffer.skipSample();
haveNext = rollingBuffer.peekSample(sampleBuffer);
}
}
if (!haveNext) {
return false;
}
if (spliceOutTimeUs != Long.MIN_VALUE && sampleBuffer.timeUs >= spliceOutTimeUs) {
return false;
}
return true;
}
// Called by the loading thread.
@Override
public void format(Format format) {
rollingBuffer.format(format);
}
@Override
public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
return rollingBuffer.sampleData(input, length, allowEndOfInput);
}
@Override
public void sampleData(ParsableByteArray buffer, int length) {
rollingBuffer.sampleData(buffer, length);
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
rollingBuffer.sampleMetadata(timeUs, flags, size, offset, encryptionKey);
}
}

View File

@ -46,11 +46,13 @@ public final class RollingSampleBuffer implements TrackOutput {
// Accessed only by the consuming thread.
private long totalBytesDropped;
// Accessed only by the loading thread.
// Accessed only by the loading thread (or the consuming thread when there is no loading thread).
private long sampleOffsetUs;
private long totalBytesWritten;
private Allocation lastAllocation;
private int lastAllocationOffset;
private boolean needKeyframe;
private boolean pendingSplice;
// Accessed by both the loading and consuming threads.
private volatile Format upstreamFormat;
@ -66,6 +68,7 @@ public final class RollingSampleBuffer implements TrackOutput {
extrasHolder = new BufferExtrasHolder();
scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);
lastAllocationOffset = allocationLength;
needKeyframe = true;
}
// Called by the consuming thread, but only when there is no loading thread.
@ -82,6 +85,15 @@ public final class RollingSampleBuffer implements TrackOutput {
totalBytesWritten = 0;
lastAllocation = null;
lastAllocationOffset = allocationLength;
needKeyframe = true;
}
/**
* Indicates that samples subsequently queued to the buffer should be spliced into those already
* queued.
*/
public void splice() {
pendingSplice = true;
}
/**
@ -172,27 +184,6 @@ public final class RollingSampleBuffer implements TrackOutput {
return infoQueue.getLargestQueuedTimestampUs();
}
/**
* Fills {@code buffer} with information about the current sample, but does not write its data.
* <p>
* Populates {@link DecoderInputBuffer#size}, {@link DecoderInputBuffer#timeUs} and the buffer
* flags.
*
* @param buffer The buffer into which the current sample information should be written.
* @return True if the buffer was filled. False if there is no current sample.
*/
public boolean peekSample(DecoderInputBuffer buffer) {
return infoQueue.peekSample(buffer, extrasHolder);
}
/**
* Skips the current sample.
*/
public void skipSample() {
long nextOffset = infoQueue.moveToNextSample();
dropDownstreamTo(nextOffset);
}
/**
* Skips all currently buffered samples.
*/
@ -227,7 +218,7 @@ public final class RollingSampleBuffer implements TrackOutput {
*/
public boolean readSample(DecoderInputBuffer buffer) {
// Write the sample information into the buffer and extrasHolder.
boolean haveSample = infoQueue.peekSample(buffer, extrasHolder);
boolean haveSample = infoQueue.readSample(buffer, extrasHolder);
if (!haveSample) {
return false;
}
@ -240,8 +231,7 @@ public final class RollingSampleBuffer implements TrackOutput {
buffer.ensureSpaceForWrite(buffer.size);
readData(extrasHolder.offset, buffer.data, buffer.size);
// Advance the read head.
long nextOffset = infoQueue.moveToNextSample();
dropDownstreamTo(nextOffset);
dropDownstreamTo(extrasHolder.nextOffset);
return true;
}
@ -396,7 +386,7 @@ public final class RollingSampleBuffer implements TrackOutput {
*/
public void formatWithOffset(Format format, long sampleOffsetUs) {
this.sampleOffsetUs = sampleOffsetUs;
upstreamFormat = getAdjustedSampleFormat(format, sampleOffsetUs);
format(format);
}
@Override
@ -435,6 +425,22 @@ public final class RollingSampleBuffer implements TrackOutput {
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
if (pendingSplice) {
if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !infoQueue.attemptSplice(timeUs)) {
return;
}
// TODO - We should be able to actually remove the data from the rolling buffer after a splice
// succeeds, but doing so is a little bit tricky; it requires moving data written after the
// last committed sample.
pendingSplice = false;
}
if (needKeyframe) {
if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) {
// TODO - As above, although this case is probably less worthwhile.
return;
}
needKeyframe = false;
}
timeUs += sampleOffsetUs;
long absoluteOffset = totalBytesWritten - size - offset;
infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey, upstreamFormat);
@ -601,7 +607,8 @@ public final class RollingSampleBuffer implements TrackOutput {
/**
* Fills {@code buffer} with information about the current sample, but does not write its data.
* The absolute position of the sample's data in the rolling buffer is stored in
* {@code extrasHolder}.
* {@code extrasHolder}, along with an encryption id if present, and the absolute position of
* the first byte that may still be required after the current sample has been read.
* <p>
* Populates {@link DecoderInputBuffer#size}, {@link DecoderInputBuffer#timeUs}, the buffer
* flags and {@code extrasHolder}.
@ -610,7 +617,7 @@ public final class RollingSampleBuffer implements TrackOutput {
* @param extrasHolder The holder into which extra sample information should be written.
* @return True if the buffer and extras were filled. False if there is no current sample.
*/
public synchronized boolean peekSample(DecoderInputBuffer buffer,
public synchronized boolean readSample(DecoderInputBuffer buffer,
BufferExtrasHolder extrasHolder) {
if (queueSize == 0) {
return false;
@ -620,26 +627,19 @@ public final class RollingSampleBuffer implements TrackOutput {
buffer.setFlags(flags[relativeReadIndex]);
extrasHolder.offset = offsets[relativeReadIndex];
extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex];
return true;
}
/**
* Advances the read index to the next sample.
*
* @return The absolute position of the first byte in the rolling buffer that may still be
* required after advancing the index. Data prior to this position can be dropped.
*/
public synchronized long moveToNextSample() {
largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs);
queueSize--;
largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, timesUs[relativeReadIndex]);
int lastReadIndex = relativeReadIndex++;
relativeReadIndex++;
absoluteReadIndex++;
if (relativeReadIndex == capacity) {
// Wrap around.
relativeReadIndex = 0;
}
return queueSize > 0 ? offsets[relativeReadIndex]
: (sizes[lastReadIndex] + offsets[lastReadIndex]);
extrasHolder.nextOffset = queueSize > 0 ? offsets[relativeReadIndex]
: extrasHolder.offset + buffer.size;
return true;
}
/**
@ -760,6 +760,26 @@ public final class RollingSampleBuffer implements TrackOutput {
}
}
/**
* Attempts to discard samples from the tail of the queue to allow samples starting from the
* specified timestamp to be spliced in.
*
* @param timeUs The timestamp at which the splice occurs.
* @return Whether the splice was successful.
*/
public synchronized boolean attemptSplice(long timeUs) {
if (largestDequeuedTimestampUs >= timeUs) {
return false;
}
int retainCount = queueSize;
while (retainCount > 0
&& timesUs[(relativeReadIndex + retainCount - 1) % capacity] >= timeUs) {
retainCount--;
}
discardUpstreamSamples(absoluteReadIndex + retainCount);
return true;
}
}
/**
@ -768,6 +788,7 @@ public final class RollingSampleBuffer implements TrackOutput {
private static final class BufferExtrasHolder {
public long offset;
public long nextOffset;
public byte[] encryptionKeyId;
}

View File

@ -399,19 +399,16 @@ public class HlsChunkSource {
Format format = variants[variantIndex].format;
// Configure the extractor that will read the chunk.
HlsExtractorWrapper extractorWrapper;
Extractor extractor;
boolean extractorNeedsInit = true;
String lastPathSegment = chunkUri.getLastPathSegment();
if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) {
// TODO: Inject a timestamp adjuster and use it along with ID3 PRIV tag values with owner
// identifier com.apple.streaming.transportStreamTimestamp. This may also apply to the MP3
// case below.
Extractor extractor = new AdtsExtractor(startTimeUs);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariant);
extractor = new AdtsExtractor(startTimeUs);
} else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {
Extractor extractor = new Mp3Extractor(startTimeUs);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariant);
extractor = new Mp3Extractor(startTimeUs);
} else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
|| lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {
PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(false,
@ -422,9 +419,7 @@ public class HlsChunkSource {
// a discontinuity sequence greater than the one that this source is trying to start at.
return;
}
Extractor extractor = new WebvttExtractor(format.language, timestampAdjuster);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariant);
extractor = new WebvttExtractor(format.language, timestampAdjuster);
} else if (previous == null
|| previous.discontinuitySequenceNumber != segment.discontinuitySequenceNumber
|| format != previous.format) {
@ -448,17 +443,16 @@ public class HlsChunkSource {
workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_H264_STREAM;
}
}
Extractor extractor = new TsExtractor(timestampAdjuster, workaroundFlags);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariant);
extractor = new TsExtractor(timestampAdjuster, workaroundFlags);
} else {
// MPEG-2 TS segments, and we need to continue using the same extractor.
extractorWrapper = previous.extractorWrapper;
extractor = previous.extractor;
extractorNeedsInit = false;
}
out.chunk = new TsChunk(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs,
chunkMediaSequence, segment.discontinuitySequenceNumber, extractorWrapper, encryptionKey,
encryptionIv);
chunkMediaSequence, segment.discontinuitySequenceNumber, extractor, extractorNeedsInit,
switchingVariant, encryptionKey, encryptionIv);
}
/**
@ -584,15 +578,15 @@ public class HlsChunkSource {
private int getNextVariantIndex(TsChunk previous, long playbackPositionUs) {
clearStaleBlacklistedVariants();
long bufferedDurationUs;
if (previous != null) {
// Use start time of the previous chunk rather than its end time because switching format will
// require downloading overlapping segments.
bufferedDurationUs = Math.max(0, previous.startTimeUs - playbackPositionUs);
} else {
bufferedDurationUs = 0;
}
if (enabledVariants.length > 1) {
long bufferedDurationUs;
if (previous != null) {
// Use start time of the previous chunk rather than its end time because switching format
// will require downloading overlapping segments.
bufferedDurationUs = Math.max(0, previous.startTimeUs - playbackPositionUs);
} else {
bufferedDurationUs = 0;
}
adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, enabledVariantBlacklistFlags,
evaluation);
} else {

View File

@ -1,252 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.DecoderInputBuffer;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.DefaultTrackOutput;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.upstream.Allocator;
import com.google.android.exoplayer.util.Assertions;
import android.util.SparseArray;
import java.io.IOException;
/**
* Wraps a {@link Extractor}, adding functionality to enable reading of the extracted samples.
*/
public final class HlsExtractorWrapper implements ExtractorOutput {
public final int trigger;
public final Format format;
public final long startTimeUs;
private final Extractor extractor;
private final SparseArray<DefaultTrackOutput> sampleQueues;
private final boolean shouldSpliceIn;
private Allocator allocator;
private volatile boolean tracksBuilt;
// Accessed only by the consuming thread.
private boolean prepared;
private boolean spliceConfigured;
public HlsExtractorWrapper(int trigger, Format format, long startTimeUs, Extractor extractor,
boolean shouldSpliceIn) {
this.trigger = trigger;
this.format = format;
this.startTimeUs = startTimeUs;
this.extractor = extractor;
this.shouldSpliceIn = shouldSpliceIn;
sampleQueues = new SparseArray<>();
}
/**
* Initializes the wrapper for use.
*
* @param allocator An allocator for obtaining allocations into which extracted data is written.
*/
public void init(Allocator allocator) {
this.allocator = allocator;
extractor.init(this);
}
/**
* Whether the extractor is prepared.
*
* @return True if the extractor is prepared. False otherwise.
*/
public boolean isPrepared() {
if (!prepared && tracksBuilt) {
for (int i = 0; i < sampleQueues.size(); i++) {
if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
return false;
}
}
prepared = true;
}
return prepared;
}
/**
* Clears queues for all tracks, returning all allocations to the allocator.
*/
public void clear() {
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).clear();
}
}
/**
* Gets the largest timestamp of any sample parsed by the extractor.
*
* @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed.
*/
public long getLargestParsedTimestampUs() {
long largestParsedTimestampUs = Long.MIN_VALUE;
for (int i = 0; i < sampleQueues.size(); i++) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs,
sampleQueues.valueAt(i).getLargestParsedTimestampUs());
}
return largestParsedTimestampUs;
}
/**
* Attempts to configure a splice from this extractor to the next.
* <p>
* The splice is performed such that for each track the samples read from the next extractor
* start with a keyframe, and continue from where the samples read from this extractor finish.
* A successful splice may discard samples from either or both extractors.
* <p>
* Splice configuration may fail if the next extractor is not yet in a state that allows the
* splice to be performed. Calling this method is a noop if the splice has already been
* configured. Hence this method should be called repeatedly during the window within which a
* splice can be performed.
* <p>
* This method must only be called after the extractor has been prepared.
*
* @param nextExtractor The extractor being spliced to.
*/
public final void configureSpliceTo(HlsExtractorWrapper nextExtractor) {
Assertions.checkState(isPrepared());
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;
int trackCount = getTrackCount();
for (int i = 0; i < trackCount; i++) {
DefaultTrackOutput currentSampleQueue = sampleQueues.valueAt(i);
DefaultTrackOutput nextSampleQueue = nextExtractor.sampleQueues.valueAt(i);
spliceConfigured &= currentSampleQueue.configureSpliceTo(nextSampleQueue);
}
this.spliceConfigured = spliceConfigured;
return;
}
/**
* Gets the number of available tracks.
* <p>
* This method must only be called after the extractor has been prepared.
*
* @return The number of available tracks.
*/
public int getTrackCount() {
Assertions.checkState(isPrepared());
return sampleQueues.size();
}
/**
* Gets the {@link Format} of the samples belonging to a specified track.
* <p>
* This method must only be called after the extractor has been prepared.
*
* @param track The track index.
* @return The corresponding sample format.
*/
public Format getSampleFormat(int track) {
Assertions.checkState(isPrepared());
return sampleQueues.valueAt(track).getUpstreamFormat();
}
/**
* Gets the next sample for the specified track.
* <p>
* This method must only be called after the extractor has been prepared.
*
* @param track The track from which to read.
* @param buffer A {@link DecoderInputBuffer} to populate with a sample.
* @return True if a sample was read. False otherwise.
*/
public boolean getSample(int track, DecoderInputBuffer buffer) {
Assertions.checkState(isPrepared());
return sampleQueues.valueAt(track).getSample(buffer);
}
/**
* Discards all samples for the specified track.
* <p>
* This method must only be called after the extractor has been prepared.
*
* @param track The track from which samples should be discarded.
*/
public void discardSamplesForTrack(int track) {
Assertions.checkState(isPrepared());
sampleQueues.valueAt(track).skipAllSamples();
}
/**
* Whether samples are available for reading from {@link #getSample(int, DecoderInputBuffer)} for
* the specified track.
* <p>
* This method must only be called after the extractor has been prepared.
*
* @return True if samples are available for reading from
* {@link #getSample(int, DecoderInputBuffer)} for the specified track. False otherwise.
*/
public boolean hasSamples(int track) {
Assertions.checkState(isPrepared());
return !sampleQueues.valueAt(track).isEmpty();
}
/**
* Reads from the provided {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}.
* @throws IOException If an error occurred reading from the source.
* @throws InterruptedException If the thread was interrupted.
*/
public int read(ExtractorInput input) throws IOException, InterruptedException {
int result = extractor.read(input, null);
Assertions.checkState(result != Extractor.RESULT_SEEK);
return result;
}
// ExtractorOutput implementation.
@Override
public TrackOutput track(int id) {
DefaultTrackOutput sampleQueue = new DefaultTrackOutput(allocator);
sampleQueues.put(id, sampleQueue);
return sampleQueue;
}
@Override
public void endTracks() {
this.tracksBuilt = true;
}
@Override
public void seekMap(SeekMap seekMap) {
// Do nothing.
}
@Override
public void drmInitData(DrmInitData drmInit) {
// Do nothing.
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.RollingSampleBuffer;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.upstream.Allocator;
import android.util.SparseArray;
/**
* An {@link ExtractorOutput} for HLS playbacks.
*/
/* package */ final class HlsOutput implements ExtractorOutput {
private final Allocator allocator;
private final SparseArray<RollingSampleBuffer> sampleQueues = new SparseArray<>();
private boolean prepared;
private RollingSampleBuffer[] trackOutputArray;
private volatile boolean tracksBuilt;
public HlsOutput(Allocator allocator) {
this.allocator = allocator;
}
// Called by the consuming thread.
/**
* Prepares the output, or does nothing if the output is already prepared.
*
* @return True if the output is prepared, false otherwise.
*/
public boolean prepare() {
if (prepared) {
return true;
} else if (!tracksBuilt) {
return false;
} else {
if (trackOutputArray == null) {
trackOutputArray = new RollingSampleBuffer[sampleQueues.size()];
for (int i = 0; i < trackOutputArray.length; i++) {
trackOutputArray[i] = sampleQueues.valueAt(i);
}
}
for (RollingSampleBuffer sampleQueue : trackOutputArray) {
if (sampleQueue.getUpstreamFormat() == null) {
return false;
}
}
prepared = true;
return true;
}
}
/**
* Returns the array of track outputs, or null if the output is not yet prepared.
*/
public RollingSampleBuffer[] getTrackOutputs() {
return trackOutputArray;
}
// Called by the consuming thread, but only when there is no loading thread.
/**
* Clears all track outputs.
*/
public void clear() {
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).clear();
}
}
/**
* Indicates to all track outputs that they should splice in subsequently queued samples.
*/
public void splice() {
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).splice();
}
}
// ExtractorOutput implementation. Called by the loading thread.
@Override
public RollingSampleBuffer track(int id) {
if (sampleQueues.indexOfKey(id) >= 0) {
return sampleQueues.get(id);
}
RollingSampleBuffer trackOutput = new RollingSampleBuffer(allocator);
sampleQueues.put(id, trackOutput);
return trackOutput;
}
@Override
public void endTracks() {
tracksBuilt = true;
}
@Override
public void seekMap(SeekMap seekMap) {
// Do nothing.
}
@Override
public void drmInitData(DrmInitData drmInitData) {
// Do nothing.
}
}

View File

@ -29,6 +29,7 @@ import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkHolder;
import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener;
import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener.EventDispatcher;
import com.google.android.exoplayer.extractor.RollingSampleBuffer;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
@ -59,7 +60,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
private final Loader loader;
private final HlsChunkSource chunkSource;
private final LinkedList<HlsExtractorWrapper> extractors;
private final LinkedList<TsChunk> tsChunks = new LinkedList<TsChunk>();
private final HlsOutput output;
private final int bufferSizeContribution;
private final ChunkHolder nextChunkHolder;
private final EventDispatcher eventDispatcher;
@ -69,6 +71,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
private boolean seenFirstTrackSelection;
private int enabledTrackCount;
private RollingSampleBuffer[] trackOutputs;
private Format downstreamFormat;
// Tracks are complicated in HLS. See documentation of buildTracks for details.
@ -112,7 +115,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
this.pendingResetPositionUs = C.UNSET_TIME_US;
loader = new Loader("Loader:HLS", minLoadableRetryCount);
eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId);
extractors = new LinkedList<>();
output = new HlsOutput(loadControl.getAllocator());
nextChunkHolder = new ChunkHolder();
}
@ -131,20 +134,11 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
prepared = true;
return true;
}
if (!extractors.isEmpty()) {
while (true) {
// We're not prepared, but we might have loaded what we need.
HlsExtractorWrapper extractor = extractors.getFirst();
if (extractor.isPrepared()) {
buildTracks(extractor);
prepared = true;
return true;
} else if (extractors.size() > 1) {
extractors.removeFirst().clear();
} else {
break;
}
}
if (output.prepare()) {
trackOutputs = output.getTrackOutputs();
buildTracks();
prepared = true;
return true;
}
// We're not prepared.
maybeThrowError();
@ -223,9 +217,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
@Override
public void continueBuffering(long playbackPositionUs) {
downstreamPositionUs = playbackPositionUs;
if (!extractors.isEmpty()) {
discardSamplesForDisabledTracks(getCurrentExtractor());
}
discardSamplesForDisabledTracks();
if (!loader.isLoading()) {
maybeStartLoading();
}
@ -238,20 +230,17 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
} else if (loadingFinished) {
return C.END_OF_SOURCE_US;
} else {
long bufferedPositionUs = extractors.getLast().getLargestParsedTimestampUs();
if (extractors.size() > 1) {
// When adapting from one format to the next, the penultimate extractor may have the largest
// parsed timestamp (e.g. if the last extractor hasn't parsed any timestamps yet).
bufferedPositionUs = Math.max(bufferedPositionUs,
extractors.get(extractors.size() - 2).getLargestParsedTimestampUs());
}
long bufferedPositionUs = downstreamPositionUs;
if (previousTsLoadable != null) {
// Buffered position should be at least as large as the end time of the previously loaded
// chunk.
bufferedPositionUs = Math.max(previousTsLoadable.endTimeUs, bufferedPositionUs);
}
return bufferedPositionUs == Long.MIN_VALUE ? downstreamPositionUs
: bufferedPositionUs;
for (RollingSampleBuffer trackOutput : trackOutputs) {
bufferedPositionUs = Math.max(bufferedPositionUs,
trackOutput.getLargestQueuedTimestampUs());
}
return bufferedPositionUs;
}
}
@ -276,19 +265,10 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
if (loadingFinished) {
return true;
}
if (isPendingReset() || extractors.isEmpty()) {
if (isPendingReset()) {
return false;
}
for (int extractorIndex = 0; extractorIndex < extractors.size(); extractorIndex++) {
HlsExtractorWrapper extractor = extractors.get(extractorIndex);
if (!extractor.isPrepared()) {
break;
}
if (extractor.hasSamples(group)) {
return true;
}
}
return false;
return !trackOutputs[group].isEmpty();
}
/* package */ void maybeThrowError() throws IOException {
@ -309,53 +289,41 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
return TrackStream.NOTHING_READ;
}
HlsExtractorWrapper extractor = getCurrentExtractor();
if (!extractor.isPrepared()) {
TsChunk currentChunk = tsChunks.getFirst();
Format currentFormat = currentChunk.format;
if (downstreamFormat == null || !downstreamFormat.equals(currentFormat)) {
eventDispatcher.downstreamFormatChanged(currentFormat, currentChunk.trigger,
currentChunk.startTimeUs);
downstreamFormat = currentFormat;
}
RollingSampleBuffer sampleQueue = trackOutputs[group];
if (sampleQueue.isEmpty()) {
if (loadingFinished) {
buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
return TrackStream.BUFFER_READ;
}
return TrackStream.NOTHING_READ;
}
if (downstreamFormat == null || !downstreamFormat.equals(extractor.format)) {
// Notify a change in the downstream format.
eventDispatcher.downstreamFormatChanged(extractor.format, extractor.trigger,
extractor.startTimeUs);
downstreamFormat = extractor.format;
}
if (extractors.size() > 1) {
// If there's more than one extractor, attempt to configure a seamless splice from the
// current one to the next one.
extractor.configureSpliceTo(extractors.get(1));
}
int extractorIndex = 0;
while (extractors.size() > extractorIndex + 1 && !extractor.hasSamples(group)) {
// 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 TrackStream.NOTHING_READ;
}
}
Format sampleFormat = extractor.getSampleFormat(group);
if (sampleFormat != null && !sampleFormat.equals(downstreamSampleFormats[group])) {
Format sampleFormat = sampleQueue.getDownstreamFormat();
if (!sampleFormat.equals(downstreamSampleFormats[group])) {
formatHolder.format = sampleFormat;
downstreamSampleFormats[group] = sampleFormat;
return TrackStream.FORMAT_READ;
}
if (extractor.getSample(group, buffer)) {
if (buffer.timeUs < lastSeekPositionUs) {
if (sampleQueue.readSample(buffer)) {
long sampleTimeUs = buffer.timeUs;
while (tsChunks.size() > 1 && tsChunks.get(1).startTimeUs <= sampleTimeUs) {
tsChunks.removeFirst();
}
if (sampleTimeUs < lastSeekPositionUs) {
buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
}
return TrackStream.BUFFER_READ;
}
if (loadingFinished) {
buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
return TrackStream.BUFFER_READ;
}
return TrackStream.NOTHING_READ;
}
@ -441,17 +409,15 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
* effect of selecting an extractor track, leaving the selected track on the chunk source
* unchanged.</li>
* </ul>
*
* @param extractor The prepared extractor.
*/
private void buildTracks(HlsExtractorWrapper extractor) {
private void buildTracks() {
// Iterate through the extractor tracks to discover the "primary" track type, and the index
// of the single track of this type.
int primaryExtractorTrackType = PRIMARY_TYPE_NONE;
int primaryExtractorTrackIndex = -1;
int extractorTrackCount = extractor.getTrackCount();
int extractorTrackCount = trackOutputs.length;
for (int i = 0; i < extractorTrackCount; i++) {
String sampleMimeType = extractor.getSampleFormat(i).sampleMimeType;
String sampleMimeType = trackOutputs[i].getUpstreamFormat().sampleMimeType;
int trackType;
if (MimeTypes.isVideo(sampleMimeType)) {
trackType = PRIMARY_TYPE_VIDEO;
@ -484,7 +450,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
// Construct the set of exposed track groups.
TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount];
for (int i = 0; i < extractorTrackCount; i++) {
Format sampleFormat = extractor.getSampleFormat(i);
Format sampleFormat = trackOutputs[i].getUpstreamFormat();
if (i == primaryExtractorTrackIndex) {
Format[] formats = new Format[chunkSourceTrackCount];
for (int j = 0; j < chunkSourceTrackCount; j++) {
@ -550,49 +516,17 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
restartFrom(positionUs);
}
/**
* Gets the current extractor from which samples should be read.
* <p>
* Calling this method discards extractors without any samples from the front of the queue. The
* last extractor is retained even if it doesn't have any samples.
* <p>
* This method must not be called unless {@link #extractors} is non-empty.
*
* @return The current extractor from which samples should be read. Guaranteed to be non-null.
*/
private HlsExtractorWrapper getCurrentExtractor() {
HlsExtractorWrapper extractor = extractors.getFirst();
while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) {
// We're finished reading from the extractor for all tracks, and so can discard it.
extractors.removeFirst().clear();
extractor = extractors.getFirst();
}
return extractor;
}
private void discardSamplesForDisabledTracks(HlsExtractorWrapper extractor) {
if (!extractor.isPrepared()) {
private void discardSamplesForDisabledTracks() {
if (!output.prepare()) {
return;
}
for (int i = 0; i < groupEnabledStates.length; i++) {
if (!groupEnabledStates[i]) {
extractor.discardSamplesForTrack(i);
trackOutputs[i].skipAllSamples();
}
}
}
private boolean haveSamplesForEnabledTracks(HlsExtractorWrapper extractor) {
if (!extractor.isPrepared()) {
return false;
}
for (int i = 0; i < groupEnabledStates.length; i++) {
if (groupEnabledStates[i] && extractor.hasSamples(i)) {
return true;
}
}
return false;
}
private void restartFrom(long positionUs) {
pendingResetPositionUs = positionUs;
loadingFinished = false;
@ -605,10 +539,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
}
private void clearState() {
for (int i = 0; i < extractors.size(); i++) {
extractors.get(i).clear();
}
extractors.clear();
tsChunks.clear();
output.clear();
clearCurrentLoadable();
previousTsLoadable = null;
}
@ -651,11 +583,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
if (isPendingReset()) {
pendingResetPositionUs = C.UNSET_TIME_US;
}
HlsExtractorWrapper extractorWrapper = tsChunk.extractorWrapper;
if (extractors.isEmpty() || extractors.getLast() != extractorWrapper) {
extractorWrapper.init(loadControl.getAllocator());
extractors.addLast(extractorWrapper);
}
tsChunk.init(output);
tsChunks.addLast(tsChunk);
eventDispatcher.loadStarted(tsChunk.dataSpec.length, tsChunk.type, tsChunk.trigger,
tsChunk.format, tsChunk.startTimeUs, tsChunk.endTimeUs);
currentTsLoadable = tsChunk;

View File

@ -37,11 +37,13 @@ public final class TsChunk extends MediaChunk {
public final int discontinuitySequenceNumber;
/**
* The wrapped extractor into which this chunk is being consumed.
* The extractor into which this chunk is being consumed.
*/
public final HlsExtractorWrapper extractorWrapper;
public final Extractor extractor;
private final boolean isEncrypted;
private final boolean extractorNeedsInit;
private final boolean shouldSpliceIn;
private int bytesLoaded;
private volatile boolean loadCanceled;
@ -53,23 +55,45 @@ public final class TsChunk extends MediaChunk {
* @param format The format of the stream to which this chunk belongs.
* @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 discontinuitySequenceNumber The discontinuity sequence number of the chunk.
* @param chunkIndex The index of the chunk.
* @param extractorWrapper A wrapped extractor to parse samples from the data.
* @param discontinuitySequenceNumber The discontinuity sequence number of the chunk.
* @param extractor The extractor to parse samples from the data.
* @param extractorNeedsInit Whether the extractor needs initializing with the target
* {@link HlsOutput}.
* @param shouldSpliceIn Whether the samples parsed from this chunk should be spliced into any
* samples already queued to the {@link HlsOutput}.
* @param encryptionKey For AES encryption chunks, the encryption key.
* @param encryptionIv For AES encryption chunks, the encryption initialization vector.
*/
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, int discontinuitySequenceNumber,
HlsExtractorWrapper extractorWrapper, byte[] encryptionKey, byte[] encryptionIv) {
Extractor extractor, boolean extractorNeedsInit, boolean shouldSpliceIn,
byte[] encryptionKey, byte[] encryptionIv) {
super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, trigger, format,
startTimeUs, endTimeUs, chunkIndex);
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.extractorWrapper = extractorWrapper;
this.extractor = extractor;
this.extractorNeedsInit = extractorNeedsInit;
this.shouldSpliceIn = shouldSpliceIn;
// Note: this.dataSource and dataSource may be different.
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
}
/**
* Initializes the chunk for loading, setting the {@link HlsOutput} that will receive samples as
* they are loaded.
*
* @param output The output that will receive the loaded samples.
*/
public void init(HlsOutput output) {
if (shouldSpliceIn) {
output.splice();
}
if (extractorNeedsInit) {
extractor.init(output);
}
}
@Override
public long bytesLoaded() {
return bytesLoaded;
@ -102,7 +126,6 @@ public final class TsChunk extends MediaChunk {
loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
skipLoadedBytes = false;
}
try {
ExtractorInput input = new DefaultExtractorInput(dataSource,
loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
@ -112,7 +135,7 @@ public final class TsChunk extends MediaChunk {
try {
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractorWrapper.read(input);
result = extractor.read(input, null);
}
} finally {
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);