Commit b5bdbedf by olly Committed by Oliver Woodman

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
parent a7d78594
/*
* 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);
}
}
......@@ -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;
}
/**
......@@ -173,27 +185,6 @@ public final class RollingSampleBuffer implements TrackOutput {
}
/**
* 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.
*/
public void skipAllSamples() {
......@@ -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;
}
......
......@@ -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 {
......
/*
* 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.
}
}
/*
* 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.
}
}
......@@ -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);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment