mirror of
https://github.com/androidx/media.git
synced 2025-05-06 07:00:19 +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.
|
// 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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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.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;
|
||||||
|
@ -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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user