mirror of
https://github.com/androidx/media.git
synced 2025-05-03 21:57:46 +08:00
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:
parent
a7d7859478
commit
b5bdbedfd5
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user