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. // Accessed only by the consuming thread.
private long totalBytesDropped; 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 sampleOffsetUs;
private long totalBytesWritten; private long totalBytesWritten;
private Allocation lastAllocation; private Allocation lastAllocation;
private int lastAllocationOffset; private int lastAllocationOffset;
private boolean needKeyframe;
private boolean pendingSplice;
// Accessed by both the loading and consuming threads. // Accessed by both the loading and consuming threads.
private volatile Format upstreamFormat; private volatile Format upstreamFormat;
@ -66,6 +68,7 @@ public final class RollingSampleBuffer implements TrackOutput {
extrasHolder = new BufferExtrasHolder(); extrasHolder = new BufferExtrasHolder();
scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);
lastAllocationOffset = allocationLength; lastAllocationOffset = allocationLength;
needKeyframe = true;
} }
// Called by the consuming thread, but only when there is no loading thread. // 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; totalBytesWritten = 0;
lastAllocation = null; lastAllocation = null;
lastAllocationOffset = allocationLength; 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(); 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. * Skips all currently buffered samples.
*/ */
@ -227,7 +218,7 @@ public final class RollingSampleBuffer implements TrackOutput {
*/ */
public boolean readSample(DecoderInputBuffer buffer) { public boolean readSample(DecoderInputBuffer buffer) {
// Write the sample information into the buffer and extrasHolder. // Write the sample information into the buffer and extrasHolder.
boolean haveSample = infoQueue.peekSample(buffer, extrasHolder); boolean haveSample = infoQueue.readSample(buffer, extrasHolder);
if (!haveSample) { if (!haveSample) {
return false; return false;
} }
@ -240,8 +231,7 @@ public final class RollingSampleBuffer implements TrackOutput {
buffer.ensureSpaceForWrite(buffer.size); buffer.ensureSpaceForWrite(buffer.size);
readData(extrasHolder.offset, buffer.data, buffer.size); readData(extrasHolder.offset, buffer.data, buffer.size);
// Advance the read head. // Advance the read head.
long nextOffset = infoQueue.moveToNextSample(); dropDownstreamTo(extrasHolder.nextOffset);
dropDownstreamTo(nextOffset);
return true; return true;
} }
@ -396,7 +386,7 @@ public final class RollingSampleBuffer implements TrackOutput {
*/ */
public void formatWithOffset(Format format, long sampleOffsetUs) { public void formatWithOffset(Format format, long sampleOffsetUs) {
this.sampleOffsetUs = sampleOffsetUs; this.sampleOffsetUs = sampleOffsetUs;
upstreamFormat = getAdjustedSampleFormat(format, sampleOffsetUs); format(format);
} }
@Override @Override
@ -435,6 +425,22 @@ public final class RollingSampleBuffer implements TrackOutput {
@Override @Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { 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; timeUs += sampleOffsetUs;
long absoluteOffset = totalBytesWritten - size - offset; long absoluteOffset = totalBytesWritten - size - offset;
infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey, upstreamFormat); 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. * 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 * 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> * <p>
* Populates {@link DecoderInputBuffer#size}, {@link DecoderInputBuffer#timeUs}, the buffer * Populates {@link DecoderInputBuffer#size}, {@link DecoderInputBuffer#timeUs}, the buffer
* flags and {@code extrasHolder}. * 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. * @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. * @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) { BufferExtrasHolder extrasHolder) {
if (queueSize == 0) { if (queueSize == 0) {
return false; return false;
@ -620,26 +627,19 @@ public final class RollingSampleBuffer implements TrackOutput {
buffer.setFlags(flags[relativeReadIndex]); buffer.setFlags(flags[relativeReadIndex]);
extrasHolder.offset = offsets[relativeReadIndex]; extrasHolder.offset = offsets[relativeReadIndex];
extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex]; extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex];
return true;
}
/** largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs);
* 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() {
queueSize--; queueSize--;
largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, timesUs[relativeReadIndex]); relativeReadIndex++;
int lastReadIndex = relativeReadIndex++;
absoluteReadIndex++; absoluteReadIndex++;
if (relativeReadIndex == capacity) { if (relativeReadIndex == capacity) {
// Wrap around. // Wrap around.
relativeReadIndex = 0; 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 { private static final class BufferExtrasHolder {
public long offset; public long offset;
public long nextOffset;
public byte[] encryptionKeyId; public byte[] encryptionKeyId;
} }

View File

@ -399,19 +399,16 @@ public class HlsChunkSource {
Format format = variants[variantIndex].format; Format format = variants[variantIndex].format;
// Configure the extractor that will read the chunk. // Configure the extractor that will read the chunk.
HlsExtractorWrapper extractorWrapper; Extractor extractor;
boolean extractorNeedsInit = true;
String lastPathSegment = chunkUri.getLastPathSegment(); String lastPathSegment = chunkUri.getLastPathSegment();
if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) {
// TODO: Inject a timestamp adjuster and use it along with ID3 PRIV tag values with owner // 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 // identifier com.apple.streaming.transportStreamTimestamp. This may also apply to the MP3
// case below. // case below.
Extractor extractor = new AdtsExtractor(startTimeUs); extractor = new AdtsExtractor(startTimeUs);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariant);
} else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {
Extractor extractor = new Mp3Extractor(startTimeUs); extractor = new Mp3Extractor(startTimeUs);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariant);
} else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) } else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
|| lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {
PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(false, 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. // a discontinuity sequence greater than the one that this source is trying to start at.
return; return;
} }
Extractor extractor = new WebvttExtractor(format.language, timestampAdjuster); extractor = new WebvttExtractor(format.language, timestampAdjuster);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariant);
} else if (previous == null } else if (previous == null
|| previous.discontinuitySequenceNumber != segment.discontinuitySequenceNumber || previous.discontinuitySequenceNumber != segment.discontinuitySequenceNumber
|| format != previous.format) { || format != previous.format) {
@ -448,17 +443,16 @@ public class HlsChunkSource {
workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_H264_STREAM; workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_H264_STREAM;
} }
} }
Extractor extractor = new TsExtractor(timestampAdjuster, workaroundFlags); extractor = new TsExtractor(timestampAdjuster, workaroundFlags);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariant);
} else { } else {
// MPEG-2 TS segments, and we need to continue using the same extractor. // 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, out.chunk = new TsChunk(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs,
chunkMediaSequence, segment.discontinuitySequenceNumber, extractorWrapper, encryptionKey, chunkMediaSequence, segment.discontinuitySequenceNumber, extractor, extractorNeedsInit,
encryptionIv); switchingVariant, encryptionKey, encryptionIv);
} }
/** /**
@ -584,15 +578,15 @@ public class HlsChunkSource {
private int getNextVariantIndex(TsChunk previous, long playbackPositionUs) { private int getNextVariantIndex(TsChunk previous, long playbackPositionUs) {
clearStaleBlacklistedVariants(); 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) { 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, adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, enabledVariantBlacklistFlags,
evaluation); evaluation);
} else { } 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.ChunkHolder;
import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener; import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener;
import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener.EventDispatcher; 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;
import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions; 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 Loader loader;
private final HlsChunkSource chunkSource; 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 int bufferSizeContribution;
private final ChunkHolder nextChunkHolder; private final ChunkHolder nextChunkHolder;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
@ -69,6 +71,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
private boolean seenFirstTrackSelection; private boolean seenFirstTrackSelection;
private int enabledTrackCount; private int enabledTrackCount;
private RollingSampleBuffer[] trackOutputs;
private Format downstreamFormat; private Format downstreamFormat;
// Tracks are complicated in HLS. See documentation of buildTracks for details. // 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; this.pendingResetPositionUs = C.UNSET_TIME_US;
loader = new Loader("Loader:HLS", minLoadableRetryCount); loader = new Loader("Loader:HLS", minLoadableRetryCount);
eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId); eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId);
extractors = new LinkedList<>(); output = new HlsOutput(loadControl.getAllocator());
nextChunkHolder = new ChunkHolder(); nextChunkHolder = new ChunkHolder();
} }
@ -131,20 +134,11 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
prepared = true; prepared = true;
return true; return true;
} }
if (!extractors.isEmpty()) { if (output.prepare()) {
while (true) { trackOutputs = output.getTrackOutputs();
// We're not prepared, but we might have loaded what we need. buildTracks();
HlsExtractorWrapper extractor = extractors.getFirst(); prepared = true;
if (extractor.isPrepared()) { return true;
buildTracks(extractor);
prepared = true;
return true;
} else if (extractors.size() > 1) {
extractors.removeFirst().clear();
} else {
break;
}
}
} }
// We're not prepared. // We're not prepared.
maybeThrowError(); maybeThrowError();
@ -223,9 +217,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
@Override @Override
public void continueBuffering(long playbackPositionUs) { public void continueBuffering(long playbackPositionUs) {
downstreamPositionUs = playbackPositionUs; downstreamPositionUs = playbackPositionUs;
if (!extractors.isEmpty()) { discardSamplesForDisabledTracks();
discardSamplesForDisabledTracks(getCurrentExtractor());
}
if (!loader.isLoading()) { if (!loader.isLoading()) {
maybeStartLoading(); maybeStartLoading();
} }
@ -238,20 +230,17 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
} else if (loadingFinished) { } else if (loadingFinished) {
return C.END_OF_SOURCE_US; return C.END_OF_SOURCE_US;
} else { } else {
long bufferedPositionUs = extractors.getLast().getLargestParsedTimestampUs(); long bufferedPositionUs = downstreamPositionUs;
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());
}
if (previousTsLoadable != null) { if (previousTsLoadable != null) {
// Buffered position should be at least as large as the end time of the previously loaded // Buffered position should be at least as large as the end time of the previously loaded
// chunk. // chunk.
bufferedPositionUs = Math.max(previousTsLoadable.endTimeUs, bufferedPositionUs); bufferedPositionUs = Math.max(previousTsLoadable.endTimeUs, bufferedPositionUs);
} }
return bufferedPositionUs == Long.MIN_VALUE ? downstreamPositionUs for (RollingSampleBuffer trackOutput : trackOutputs) {
: bufferedPositionUs; bufferedPositionUs = Math.max(bufferedPositionUs,
trackOutput.getLargestQueuedTimestampUs());
}
return bufferedPositionUs;
} }
} }
@ -276,19 +265,10 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
if (loadingFinished) { if (loadingFinished) {
return true; return true;
} }
if (isPendingReset() || extractors.isEmpty()) { if (isPendingReset()) {
return false; return false;
} }
for (int extractorIndex = 0; extractorIndex < extractors.size(); extractorIndex++) { return !trackOutputs[group].isEmpty();
HlsExtractorWrapper extractor = extractors.get(extractorIndex);
if (!extractor.isPrepared()) {
break;
}
if (extractor.hasSamples(group)) {
return true;
}
}
return false;
} }
/* package */ void maybeThrowError() throws IOException { /* package */ void maybeThrowError() throws IOException {
@ -309,53 +289,41 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
return TrackStream.NOTHING_READ; return TrackStream.NOTHING_READ;
} }
HlsExtractorWrapper extractor = getCurrentExtractor(); TsChunk currentChunk = tsChunks.getFirst();
if (!extractor.isPrepared()) { 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; return TrackStream.NOTHING_READ;
} }
if (downstreamFormat == null || !downstreamFormat.equals(extractor.format)) { Format sampleFormat = sampleQueue.getDownstreamFormat();
// Notify a change in the downstream format. if (!sampleFormat.equals(downstreamSampleFormats[group])) {
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])) {
formatHolder.format = sampleFormat; formatHolder.format = sampleFormat;
downstreamSampleFormats[group] = sampleFormat; downstreamSampleFormats[group] = sampleFormat;
return TrackStream.FORMAT_READ; return TrackStream.FORMAT_READ;
} }
if (extractor.getSample(group, buffer)) { if (sampleQueue.readSample(buffer)) {
if (buffer.timeUs < lastSeekPositionUs) { 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); buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
} }
return TrackStream.BUFFER_READ; return TrackStream.BUFFER_READ;
} }
if (loadingFinished) {
buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
return TrackStream.BUFFER_READ;
}
return TrackStream.NOTHING_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 * effect of selecting an extractor track, leaving the selected track on the chunk source
* unchanged.</li> * unchanged.</li>
* </ul> * </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 // Iterate through the extractor tracks to discover the "primary" track type, and the index
// of the single track of this type. // of the single track of this type.
int primaryExtractorTrackType = PRIMARY_TYPE_NONE; int primaryExtractorTrackType = PRIMARY_TYPE_NONE;
int primaryExtractorTrackIndex = -1; int primaryExtractorTrackIndex = -1;
int extractorTrackCount = extractor.getTrackCount(); int extractorTrackCount = trackOutputs.length;
for (int i = 0; i < extractorTrackCount; i++) { for (int i = 0; i < extractorTrackCount; i++) {
String sampleMimeType = extractor.getSampleFormat(i).sampleMimeType; String sampleMimeType = trackOutputs[i].getUpstreamFormat().sampleMimeType;
int trackType; int trackType;
if (MimeTypes.isVideo(sampleMimeType)) { if (MimeTypes.isVideo(sampleMimeType)) {
trackType = PRIMARY_TYPE_VIDEO; trackType = PRIMARY_TYPE_VIDEO;
@ -484,7 +450,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
// Construct the set of exposed track groups. // Construct the set of exposed track groups.
TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount]; TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount];
for (int i = 0; i < extractorTrackCount; i++) { for (int i = 0; i < extractorTrackCount; i++) {
Format sampleFormat = extractor.getSampleFormat(i); Format sampleFormat = trackOutputs[i].getUpstreamFormat();
if (i == primaryExtractorTrackIndex) { if (i == primaryExtractorTrackIndex) {
Format[] formats = new Format[chunkSourceTrackCount]; Format[] formats = new Format[chunkSourceTrackCount];
for (int j = 0; j < chunkSourceTrackCount; j++) { for (int j = 0; j < chunkSourceTrackCount; j++) {
@ -550,49 +516,17 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
restartFrom(positionUs); restartFrom(positionUs);
} }
/** private void discardSamplesForDisabledTracks() {
* Gets the current extractor from which samples should be read. if (!output.prepare()) {
* <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()) {
return; return;
} }
for (int i = 0; i < groupEnabledStates.length; i++) { for (int i = 0; i < groupEnabledStates.length; i++) {
if (!groupEnabledStates[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) { private void restartFrom(long positionUs) {
pendingResetPositionUs = positionUs; pendingResetPositionUs = positionUs;
loadingFinished = false; loadingFinished = false;
@ -605,10 +539,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
} }
private void clearState() { private void clearState() {
for (int i = 0; i < extractors.size(); i++) { tsChunks.clear();
extractors.get(i).clear(); output.clear();
}
extractors.clear();
clearCurrentLoadable(); clearCurrentLoadable();
previousTsLoadable = null; previousTsLoadable = null;
} }
@ -651,11 +583,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
if (isPendingReset()) { if (isPendingReset()) {
pendingResetPositionUs = C.UNSET_TIME_US; pendingResetPositionUs = C.UNSET_TIME_US;
} }
HlsExtractorWrapper extractorWrapper = tsChunk.extractorWrapper; tsChunk.init(output);
if (extractors.isEmpty() || extractors.getLast() != extractorWrapper) { tsChunks.addLast(tsChunk);
extractorWrapper.init(loadControl.getAllocator());
extractors.addLast(extractorWrapper);
}
eventDispatcher.loadStarted(tsChunk.dataSpec.length, tsChunk.type, tsChunk.trigger, eventDispatcher.loadStarted(tsChunk.dataSpec.length, tsChunk.type, tsChunk.trigger,
tsChunk.format, tsChunk.startTimeUs, tsChunk.endTimeUs); tsChunk.format, tsChunk.startTimeUs, tsChunk.endTimeUs);
currentTsLoadable = tsChunk; currentTsLoadable = tsChunk;

View File

@ -37,11 +37,13 @@ public final class TsChunk extends MediaChunk {
public final int discontinuitySequenceNumber; 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 isEncrypted;
private final boolean extractorNeedsInit;
private final boolean shouldSpliceIn;
private int bytesLoaded; private int bytesLoaded;
private volatile boolean loadCanceled; 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 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 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 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 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 encryptionKey For AES encryption chunks, the encryption key.
* @param encryptionIv For AES encryption chunks, the encryption initialization vector. * @param encryptionIv For AES encryption chunks, the encryption initialization vector.
*/ */
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, 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, super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, trigger, format,
startTimeUs, endTimeUs, chunkIndex); startTimeUs, endTimeUs, chunkIndex);
this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.extractorWrapper = extractorWrapper; this.extractor = extractor;
this.extractorNeedsInit = extractorNeedsInit;
this.shouldSpliceIn = shouldSpliceIn;
// Note: this.dataSource and dataSource may be different. // Note: this.dataSource and dataSource may be different.
this.isEncrypted = this.dataSource instanceof Aes128DataSource; 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 @Override
public long bytesLoaded() { public long bytesLoaded() {
return bytesLoaded; return bytesLoaded;
@ -102,7 +126,6 @@ public final class TsChunk extends MediaChunk {
loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
skipLoadedBytes = false; skipLoadedBytes = false;
} }
try { try {
ExtractorInput input = new DefaultExtractorInput(dataSource, ExtractorInput input = new DefaultExtractorInput(dataSource,
loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
@ -112,7 +135,7 @@ public final class TsChunk extends MediaChunk {
try { try {
int result = Extractor.RESULT_CONTINUE; int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractorWrapper.read(input); result = extractor.read(input, null);
} }
} finally { } finally {
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);