mirror of
https://github.com/samsonjs/media.git
synced 2026-03-27 09:45:47 +00:00
Finally - Remove Sample, fix GC churn + inefficient memory usage.
Use of Sample objects was inefficient for several reasons: - Lots of objects (1 per sample, obviously). - When switching up bitrates, there was a tendency for all Sample instances to need to expand, which effectively led to our whole media buffer being GC'd as each Sample discarded its byte[] to obtain a larger one. - When a keyframe was encountered, the Sample would typically need to expand to accommodate it. Over time, this would lead to a gradual increase in the population of Samples that were sized to accommodate keyframes. These Sample instances were then typically underutilized whenever recycled to hold a non-keyframe, leading to inefficient memory usage. This CL introduces RollingBuffer, which tightly packs pending sample data into a byte[]s obtained from an underlying BufferPool. Which fixes all of the above. There is still an issue where the total memory allocation may grow when switching up bitrate, but we can easily fix that from this point, if we choose to restrict the buffer based on allocation size rather than time. Issue: #278
This commit is contained in:
parent
28166d8c0d
commit
37e6946cd9
12 changed files with 518 additions and 301 deletions
|
|
@ -17,10 +17,10 @@ package com.google.android.exoplayer.hls;
|
|||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.hls.parser.SamplePool;
|
||||
import com.google.android.exoplayer.hls.parser.TsExtractor;
|
||||
import com.google.android.exoplayer.upstream.Aes128DataSource;
|
||||
import com.google.android.exoplayer.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.BufferPool;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException;
|
||||
|
|
@ -102,7 +102,7 @@ public class HlsChunkSource {
|
|||
private static final String TAG = "HlsChunkSource";
|
||||
private static final float BANDWIDTH_FRACTION = 0.8f;
|
||||
|
||||
private final SamplePool samplePool = new SamplePool();
|
||||
private final BufferPool bufferPool;
|
||||
private final DataSource upstreamDataSource;
|
||||
private final HlsPlaylistParser playlistParser;
|
||||
private final Variant[] enabledVariants;
|
||||
|
|
@ -165,6 +165,7 @@ public class HlsChunkSource {
|
|||
maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000;
|
||||
baseUri = playlist.baseUri;
|
||||
playlistParser = new HlsPlaylistParser();
|
||||
bufferPool = new BufferPool(256 * 1024);
|
||||
|
||||
if (playlist.type == HlsPlaylist.TYPE_MEDIA) {
|
||||
enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)};
|
||||
|
|
@ -324,7 +325,7 @@ public class HlsChunkSource {
|
|||
// Configure the extractor that will read the chunk.
|
||||
TsExtractor extractor;
|
||||
if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) {
|
||||
extractor = new TsExtractor(startTimeUs, samplePool, switchingVariantSpliced);
|
||||
extractor = new TsExtractor(startTimeUs, switchingVariantSpliced, bufferPool);
|
||||
} else {
|
||||
extractor = previousTsChunk.extractor;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls.parser;
|
|||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.upstream.BufferPool;
|
||||
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
import com.google.android.exoplayer.util.ParsableBitArray;
|
||||
|
|
@ -44,7 +45,7 @@ import java.util.Collections;
|
|||
private int bytesRead;
|
||||
|
||||
// Used to find the header.
|
||||
private boolean lastByteWasOxFF;
|
||||
private boolean lastByteWasFF;
|
||||
private boolean hasCrc;
|
||||
|
||||
// Parsed from the header.
|
||||
|
|
@ -54,8 +55,8 @@ import java.util.Collections;
|
|||
// Used when reading the samples.
|
||||
private long timeUs;
|
||||
|
||||
public AdtsReader(SamplePool samplePool) {
|
||||
super(samplePool);
|
||||
public AdtsReader(BufferPool bufferPool) {
|
||||
super(bufferPool);
|
||||
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
|
||||
state = STATE_FINDING_SYNC;
|
||||
}
|
||||
|
|
@ -77,7 +78,7 @@ import java.util.Collections;
|
|||
int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
|
||||
if (continueRead(data, adtsScratch.getData(), targetLength)) {
|
||||
parseHeader();
|
||||
startSample(Sample.TYPE_AUDIO, timeUs);
|
||||
startSample(timeUs);
|
||||
bytesRead = 0;
|
||||
state = STATE_READING_SAMPLE;
|
||||
}
|
||||
|
|
@ -130,9 +131,9 @@ import java.util.Collections;
|
|||
int startOffset = pesBuffer.getPosition();
|
||||
int endOffset = pesBuffer.limit();
|
||||
for (int i = startOffset; i < endOffset; i++) {
|
||||
boolean byteIsOxFF = (adtsData[i] & 0xFF) == 0xFF;
|
||||
boolean found = lastByteWasOxFF && !byteIsOxFF && (adtsData[i] & 0xF0) == 0xF0;
|
||||
lastByteWasOxFF = byteIsOxFF;
|
||||
boolean byteIsFF = (adtsData[i] & 0xFF) == 0xFF;
|
||||
boolean found = lastByteWasFF && !byteIsFF && (adtsData[i] & 0xF0) == 0xF0;
|
||||
lastByteWasFF = byteIsFF;
|
||||
if (found) {
|
||||
hasCrc = (adtsData[i] & 0x1) == 0;
|
||||
pesBuffer.setPosition(i + 1);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls.parser;
|
|||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.mp4.Mp4Util;
|
||||
import com.google.android.exoplayer.upstream.BufferPool;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
import com.google.android.exoplayer.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
|
@ -36,43 +37,54 @@ import java.util.List;
|
|||
private static final int NAL_UNIT_TYPE_AUD = 9;
|
||||
|
||||
private final SeiReader seiReader;
|
||||
private final ParsableByteArray pendingSampleWrapper;
|
||||
|
||||
public H264Reader(SamplePool samplePool, SeiReader seiReader) {
|
||||
super(samplePool);
|
||||
// TODO: Ideally we wouldn't need to have a copy step through a byte array here.
|
||||
private byte[] pendingSampleData;
|
||||
private int pendingSampleSize;
|
||||
private long pendingSampleTimeUs;
|
||||
|
||||
public H264Reader(BufferPool bufferPool, SeiReader seiReader) {
|
||||
super(bufferPool);
|
||||
this.seiReader = seiReader;
|
||||
this.pendingSampleData = new byte[1024];
|
||||
this.pendingSampleWrapper = new ParsableByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
|
||||
while (data.bytesLeft() > 0) {
|
||||
if (readToNextAudUnit(data, pesTimeUs)) {
|
||||
// TODO: Allowing access to the Sample object here is messy. Fix this.
|
||||
Sample pendingSample = getPendingSample();
|
||||
byte[] pendingSampleData = pendingSample.data;
|
||||
int pendingSampleSize = pendingSample.size;
|
||||
|
||||
// Scan the sample to find relevant NAL units.
|
||||
int position = 0;
|
||||
int idrNalUnitPosition = Integer.MAX_VALUE;
|
||||
while (position < pendingSampleSize) {
|
||||
position = Mp4Util.findNalUnit(pendingSampleData, position, pendingSampleSize);
|
||||
if (position < pendingSampleSize) {
|
||||
int type = Mp4Util.getNalUnitType(pendingSampleData, position);
|
||||
if (type == NAL_UNIT_TYPE_IDR) {
|
||||
idrNalUnitPosition = position;
|
||||
} else if (type == NAL_UNIT_TYPE_SEI) {
|
||||
seiReader.read(pendingSampleData, position, pendingSample.timeUs);
|
||||
}
|
||||
position += 4;
|
||||
}
|
||||
}
|
||||
|
||||
boolean isKeyframe = pendingSampleSize > idrNalUnitPosition;
|
||||
if (!hasMediaFormat() && isKeyframe) {
|
||||
parseMediaFormat(pendingSampleData, pendingSampleSize);
|
||||
}
|
||||
commitSample(isKeyframe);
|
||||
boolean sampleFinished = readToNextAudUnit(data, pesTimeUs);
|
||||
if (!sampleFinished) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Scan the sample to find relevant NAL units.
|
||||
int position = 0;
|
||||
int idrNalUnitPosition = Integer.MAX_VALUE;
|
||||
while (position < pendingSampleSize) {
|
||||
position = Mp4Util.findNalUnit(pendingSampleData, position, pendingSampleSize);
|
||||
if (position < pendingSampleSize) {
|
||||
int type = Mp4Util.getNalUnitType(pendingSampleData, position);
|
||||
if (type == NAL_UNIT_TYPE_IDR) {
|
||||
idrNalUnitPosition = position;
|
||||
} else if (type == NAL_UNIT_TYPE_SEI) {
|
||||
seiReader.read(pendingSampleData, position, pendingSampleTimeUs);
|
||||
}
|
||||
position += 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether the sample is a keyframe.
|
||||
boolean isKeyframe = pendingSampleSize > idrNalUnitPosition;
|
||||
if (!hasMediaFormat() && isKeyframe) {
|
||||
parseMediaFormat(pendingSampleData, pendingSampleSize);
|
||||
}
|
||||
|
||||
// Commit the sample to the queue.
|
||||
pendingSampleWrapper.reset(pendingSampleData, pendingSampleSize);
|
||||
appendSampleData(pendingSampleWrapper, pendingSampleSize);
|
||||
commitSample(isKeyframe);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,15 +109,17 @@ import java.util.List;
|
|||
int audOffset = Mp4Util.findNalUnit(data.data, pesOffset, pesLimit, NAL_UNIT_TYPE_AUD);
|
||||
int bytesToNextAud = audOffset - pesOffset;
|
||||
if (bytesToNextAud == 0) {
|
||||
if (!havePendingSample()) {
|
||||
startSample(Sample.TYPE_VIDEO, pesTimeUs);
|
||||
appendSampleData(data, 4);
|
||||
if (!writingSample()) {
|
||||
startSample(pesTimeUs);
|
||||
pendingSampleSize = 0;
|
||||
pendingSampleTimeUs = pesTimeUs;
|
||||
appendToSample(data, 4);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else if (havePendingSample()) {
|
||||
appendSampleData(data, bytesToNextAud);
|
||||
} else if (writingSample()) {
|
||||
appendToSample(data, bytesToNextAud);
|
||||
return data.bytesLeft() > 0;
|
||||
} else {
|
||||
data.skip(bytesToNextAud);
|
||||
|
|
@ -113,6 +127,17 @@ import java.util.List;
|
|||
}
|
||||
}
|
||||
|
||||
private void appendToSample(ParsableByteArray data, int length) {
|
||||
int requiredSize = pendingSampleSize + length;
|
||||
if (pendingSampleData.length < requiredSize) {
|
||||
byte[] newPendingSampleData = new byte[(requiredSize * 3) / 2];
|
||||
System.arraycopy(pendingSampleData, 0, newPendingSampleData, 0, pendingSampleSize);
|
||||
pendingSampleData = newPendingSampleData;
|
||||
}
|
||||
data.readBytes(pendingSampleData, pendingSampleSize, length);
|
||||
pendingSampleSize += length;
|
||||
}
|
||||
|
||||
private void parseMediaFormat(byte[] sampleData, int sampleSize) {
|
||||
// Locate the SPS and PPS units.
|
||||
int spsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_SPS);
|
||||
|
|
|
|||
|
|
@ -16,27 +16,25 @@
|
|||
package com.google.android.exoplayer.hls.parser;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.upstream.BufferPool;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
/**
|
||||
* Parses ID3 data and extracts individual text information frames.
|
||||
*/
|
||||
/* package */ class Id3Reader extends PesPayloadReader {
|
||||
|
||||
public Id3Reader(SamplePool samplePool) {
|
||||
super(samplePool);
|
||||
public Id3Reader(BufferPool bufferPool) {
|
||||
super(bufferPool);
|
||||
setMediaFormat(MediaFormat.createId3Format());
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
@Override
|
||||
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
|
||||
if (startOfPacket) {
|
||||
startSample(Sample.TYPE_MISC, pesTimeUs);
|
||||
startSample(pesTimeUs);
|
||||
}
|
||||
if (havePendingSample()) {
|
||||
if (writingSample()) {
|
||||
appendSampleData(data, data.bytesLeft());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer.hls.parser;
|
||||
|
||||
import com.google.android.exoplayer.upstream.BufferPool;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
/**
|
||||
|
|
@ -22,8 +23,8 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
|||
*/
|
||||
/* package */ abstract class PesPayloadReader extends SampleQueue {
|
||||
|
||||
protected PesPayloadReader(SamplePool samplePool) {
|
||||
super(samplePool);
|
||||
protected PesPayloadReader(BufferPool bufferPool) {
|
||||
super(bufferPool);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,301 @@
|
|||
/*
|
||||
* 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.parser;
|
||||
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.upstream.BufferPool;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.MediaExtractor;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
/**
|
||||
* A rolling buffer of sample data and corresponding sample information.
|
||||
*/
|
||||
/* package */ final class RollingSampleBuffer {
|
||||
|
||||
private final BufferPool fragmentPool;
|
||||
private final int fragmentLength;
|
||||
|
||||
private final InfoQueue infoQueue;
|
||||
private final ConcurrentLinkedQueue<byte[]> dataQueue;
|
||||
private final long[] dataOffsetHolder;
|
||||
|
||||
// Accessed only by the consuming thread.
|
||||
private long totalBytesDropped;
|
||||
|
||||
// Accessed only by the loading thread.
|
||||
private long totalBytesWritten;
|
||||
private byte[] lastFragment;
|
||||
private int lastFragmentOffset;
|
||||
private int pendingSampleSize;
|
||||
private long pendingSampleTimeUs;
|
||||
private long pendingSampleOffset;
|
||||
|
||||
public RollingSampleBuffer(BufferPool bufferPool) {
|
||||
this.fragmentPool = bufferPool;
|
||||
fragmentLength = bufferPool.bufferLength;
|
||||
infoQueue = new InfoQueue();
|
||||
dataQueue = new ConcurrentLinkedQueue<byte[]>();
|
||||
dataOffsetHolder = new long[1];
|
||||
}
|
||||
|
||||
public void release() {
|
||||
while (!dataQueue.isEmpty()) {
|
||||
fragmentPool.releaseDirect(dataQueue.remove());
|
||||
}
|
||||
}
|
||||
|
||||
// Called by the consuming thread.
|
||||
|
||||
/**
|
||||
* Fills {@code holder} with information about the current sample, but does not write its data.
|
||||
* <p>
|
||||
* The fields set are {SampleHolder#size}, {SampleHolder#timeUs} and {SampleHolder#flags}.
|
||||
*
|
||||
* @param holder The holder into which the current sample information should be written.
|
||||
* @return True if the holder was filled. False if there is no current sample.
|
||||
*/
|
||||
public boolean peekSample(SampleHolder holder) {
|
||||
return infoQueue.peekSample(holder, dataOffsetHolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the current sample.
|
||||
*/
|
||||
public void skipSample() {
|
||||
long nextOffset = infoQueue.moveToNextSample();
|
||||
dropFragmentsTo(nextOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the current sample, advancing the read index to the next sample.
|
||||
*
|
||||
* @param holder The holder into which the current sample should be written.
|
||||
*/
|
||||
public void readSample(SampleHolder holder) {
|
||||
// Write the sample information into the holder.
|
||||
infoQueue.peekSample(holder, dataOffsetHolder);
|
||||
// Write the sample data into the holder.
|
||||
if (holder.data == null || holder.data.capacity() < holder.size) {
|
||||
holder.replaceBuffer(holder.size);
|
||||
}
|
||||
if (holder.data != null) {
|
||||
readData(dataOffsetHolder[0], holder.data, holder.size);
|
||||
}
|
||||
// Advance the read head.
|
||||
long nextOffset = infoQueue.moveToNextSample();
|
||||
dropFragmentsTo(nextOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads data from the front of the rolling buffer.
|
||||
*
|
||||
* @param absolutePosition The absolute position from which data should be read.
|
||||
* @param target The buffer into which data should be written.
|
||||
* @param length The number of bytes to read.
|
||||
*/
|
||||
private void readData(long absolutePosition, ByteBuffer target, int length) {
|
||||
int remaining = length;
|
||||
while (remaining > 0) {
|
||||
dropFragmentsTo(absolutePosition);
|
||||
int positionInFragment = (int) (absolutePosition - totalBytesDropped);
|
||||
int toCopy = Math.min(remaining, fragmentLength - positionInFragment);
|
||||
target.put(dataQueue.peek(), positionInFragment, toCopy);
|
||||
absolutePosition += toCopy;
|
||||
remaining -= toCopy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard any fragments that hold data prior to the specified absolute position, returning
|
||||
* them to the pool.
|
||||
*
|
||||
* @param absolutePosition The absolute position up to which fragments can be discarded.
|
||||
*/
|
||||
private void dropFragmentsTo(long absolutePosition) {
|
||||
int relativePosition = (int) (absolutePosition - totalBytesDropped);
|
||||
int fragmentIndex = relativePosition / fragmentLength;
|
||||
for (int i = 0; i < fragmentIndex; i++) {
|
||||
fragmentPool.releaseDirect(dataQueue.remove());
|
||||
totalBytesDropped += fragmentLength;
|
||||
}
|
||||
}
|
||||
|
||||
// Called by the loading thread.
|
||||
|
||||
/**
|
||||
* Starts writing the next sample.
|
||||
*
|
||||
* @param sampleTimeUs The sample timestamp.
|
||||
*/
|
||||
public void startSample(long sampleTimeUs) {
|
||||
pendingSampleTimeUs = sampleTimeUs;
|
||||
pendingSampleOffset = totalBytesWritten;
|
||||
pendingSampleSize = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends data to the sample currently being written.
|
||||
*
|
||||
* @param buffer A buffer containing the data to append.
|
||||
* @param length The length of the data to append.
|
||||
*/
|
||||
public void appendSampleData(ParsableByteArray buffer, int length) {
|
||||
int remainingWriteLength = length;
|
||||
while (remainingWriteLength > 0) {
|
||||
if (dataQueue.isEmpty() || lastFragmentOffset == fragmentLength) {
|
||||
lastFragmentOffset = 0;
|
||||
lastFragment = fragmentPool.allocateDirect();
|
||||
dataQueue.add(lastFragment);
|
||||
}
|
||||
int thisWriteLength = Math.min(remainingWriteLength, fragmentLength - lastFragmentOffset);
|
||||
buffer.readBytes(lastFragment, lastFragmentOffset, thisWriteLength);
|
||||
lastFragmentOffset += thisWriteLength;
|
||||
remainingWriteLength -= thisWriteLength;
|
||||
}
|
||||
totalBytesWritten += length;
|
||||
pendingSampleSize += length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the sample currently being written, making it available for consumption.
|
||||
*
|
||||
* @param isKeyframe True if the sample being committed is a keyframe. False otherwise.
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
public void commitSample(boolean isKeyframe) {
|
||||
infoQueue.commitSample(pendingSampleTimeUs, pendingSampleOffset, pendingSampleSize,
|
||||
isKeyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds information about the samples in the rolling buffer.
|
||||
*/
|
||||
private static class InfoQueue {
|
||||
|
||||
private static final int SAMPLE_CAPACITY_INCREMENT = 1000;
|
||||
|
||||
private int capacity;
|
||||
|
||||
private long[] offsets;
|
||||
private int[] sizes;
|
||||
private int[] flags;
|
||||
private long[] timesUs;
|
||||
|
||||
private int queueSize;
|
||||
private int readIndex;
|
||||
private int writeIndex;
|
||||
|
||||
public InfoQueue() {
|
||||
capacity = SAMPLE_CAPACITY_INCREMENT;
|
||||
offsets = new long[capacity];
|
||||
timesUs = new long[capacity];
|
||||
flags = new int[capacity];
|
||||
sizes = new int[capacity];
|
||||
}
|
||||
|
||||
// Called by the consuming thread.
|
||||
|
||||
/**
|
||||
* Fills {@code holder} with information about the current sample, but does not write its data.
|
||||
* The first entry in {@code offsetHolder} is filled with the absolute position of the sample's
|
||||
* data in the rolling buffer.
|
||||
* <p>
|
||||
* The fields set are {SampleHolder#size}, {SampleHolder#timeUs}, {SampleHolder#flags} and
|
||||
* {@code offsetHolder[0]}.
|
||||
*
|
||||
* @param holder The holder into which the current sample information should be written.
|
||||
* @param offsetHolder The holder into which the absolute position of the sample's data should
|
||||
* be written.
|
||||
* @return True if the holders were filled. False if there is no current sample.
|
||||
*/
|
||||
public synchronized boolean peekSample(SampleHolder holder, long[] offsetHolder) {
|
||||
if (queueSize == 0) {
|
||||
return false;
|
||||
}
|
||||
holder.timeUs = timesUs[readIndex];
|
||||
holder.size = sizes[readIndex];
|
||||
holder.flags = flags[readIndex];
|
||||
offsetHolder[0] = offsets[readIndex];
|
||||
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() {
|
||||
queueSize--;
|
||||
int lastReadIndex = readIndex++;
|
||||
if (readIndex == capacity) {
|
||||
// Wrap around.
|
||||
readIndex = 0;
|
||||
}
|
||||
return queueSize > 0 ? offsets[readIndex] : (sizes[lastReadIndex] + offsets[lastReadIndex]);
|
||||
}
|
||||
|
||||
// Called by the loading thread.
|
||||
|
||||
public synchronized void commitSample(long timeUs, long offset, int size, int sampleFlags) {
|
||||
timesUs[writeIndex] = timeUs;
|
||||
offsets[writeIndex] = offset;
|
||||
sizes[writeIndex] = size;
|
||||
flags[writeIndex] = sampleFlags;
|
||||
// Increment the write index.
|
||||
queueSize++;
|
||||
if (queueSize == capacity) {
|
||||
// Increase the capacity.
|
||||
int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT;
|
||||
long[] newOffsets = new long[newCapacity];
|
||||
long[] newTimesUs = new long[newCapacity];
|
||||
int[] newFlags = new int[newCapacity];
|
||||
int[] newSizes = new int[newCapacity];
|
||||
int beforeWrap = capacity - readIndex;
|
||||
System.arraycopy(offsets, readIndex, newOffsets, 0, beforeWrap);
|
||||
System.arraycopy(timesUs, readIndex, newTimesUs, 0, beforeWrap);
|
||||
System.arraycopy(flags, readIndex, newFlags, 0, beforeWrap);
|
||||
System.arraycopy(sizes, readIndex, newSizes, 0, beforeWrap);
|
||||
int afterWrap = readIndex;
|
||||
System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
|
||||
System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
|
||||
System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
|
||||
System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
|
||||
offsets = newOffsets;
|
||||
timesUs = newTimesUs;
|
||||
flags = newFlags;
|
||||
sizes = newSizes;
|
||||
readIndex = 0;
|
||||
writeIndex = capacity;
|
||||
queueSize = capacity;
|
||||
capacity = newCapacity;
|
||||
} else {
|
||||
writeIndex++;
|
||||
if (writeIndex == capacity) {
|
||||
// Wrap around.
|
||||
writeIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,55 +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.parser;
|
||||
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
|
||||
/**
|
||||
* An internal variant of {@link SampleHolder} for internal pooling and buffering.
|
||||
*/
|
||||
/* package */ class Sample {
|
||||
|
||||
public static final int TYPE_VIDEO = 0;
|
||||
public static final int TYPE_AUDIO = 1;
|
||||
public static final int TYPE_MISC = 2;
|
||||
public static final int TYPE_COUNT = 3;
|
||||
|
||||
public final int type;
|
||||
public Sample nextInPool;
|
||||
|
||||
public byte[] data;
|
||||
public boolean isKeyframe;
|
||||
public int size;
|
||||
public long timeUs;
|
||||
|
||||
public Sample(int type, int length) {
|
||||
this.type = type;
|
||||
data = new byte[length];
|
||||
}
|
||||
|
||||
public void expand(int length) {
|
||||
byte[] newBuffer = new byte[data.length + length];
|
||||
System.arraycopy(data, 0, newBuffer, 0, size);
|
||||
data = newBuffer;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
isKeyframe = false;
|
||||
size = 0;
|
||||
timeUs = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,58 +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.parser;
|
||||
|
||||
/**
|
||||
* A pool from which the extractor can obtain sample objects for internal use.
|
||||
*
|
||||
* TODO: Over time the average size of a sample in the video pool will become larger, as the
|
||||
* proportion of samples in the pool that have at some point held a keyframe grows. Currently
|
||||
* this leads to inefficient memory usage, since samples large enough to hold keyframes end up
|
||||
* being used to hold non-keyframes. We need to fix this.
|
||||
*/
|
||||
public class SamplePool {
|
||||
|
||||
private static final int[] DEFAULT_SAMPLE_SIZES;
|
||||
static {
|
||||
DEFAULT_SAMPLE_SIZES = new int[Sample.TYPE_COUNT];
|
||||
DEFAULT_SAMPLE_SIZES[Sample.TYPE_VIDEO] = 10 * 1024;
|
||||
DEFAULT_SAMPLE_SIZES[Sample.TYPE_AUDIO] = 512;
|
||||
DEFAULT_SAMPLE_SIZES[Sample.TYPE_MISC] = 512;
|
||||
}
|
||||
|
||||
private final Sample[] pools;
|
||||
|
||||
public SamplePool() {
|
||||
pools = new Sample[Sample.TYPE_COUNT];
|
||||
}
|
||||
|
||||
/* package */ synchronized Sample get(int type) {
|
||||
if (pools[type] == null) {
|
||||
return new Sample(type, DEFAULT_SAMPLE_SIZES[type]);
|
||||
}
|
||||
Sample sample = pools[type];
|
||||
pools[type] = sample.nextInPool;
|
||||
sample.nextInPool = null;
|
||||
return sample;
|
||||
}
|
||||
|
||||
/* package */ synchronized void recycle(Sample sample) {
|
||||
sample.reset();
|
||||
sample.nextInPool = pools[sample.type];
|
||||
pools[sample.type] = sample;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -17,44 +17,49 @@ package com.google.android.exoplayer.hls.parser;
|
|||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.upstream.BufferPool;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.MediaExtractor;
|
||||
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
/**
|
||||
* Wraps a {@link RollingSampleBuffer}, adding higher level functionality such as enforcing that
|
||||
* the first sample returned from the queue is a keyframe, allowing splicing to another queue, and
|
||||
* so on.
|
||||
*/
|
||||
/* package */ abstract class SampleQueue {
|
||||
|
||||
private final SamplePool samplePool;
|
||||
private final ConcurrentLinkedQueue<Sample> internalQueue;
|
||||
private final RollingSampleBuffer rollingBuffer;
|
||||
private final SampleHolder sampleInfoHolder;
|
||||
|
||||
// Accessed only by the consuming thread.
|
||||
private boolean needKeyframe;
|
||||
private long lastReadTimeUs;
|
||||
private long spliceOutTimeUs;
|
||||
|
||||
// Accessed only by the loading thread.
|
||||
private boolean writingSample;
|
||||
|
||||
// Accessed by both the loading and consuming threads.
|
||||
private volatile MediaFormat mediaFormat;
|
||||
private volatile long largestParsedTimestampUs;
|
||||
|
||||
// Accessed by only the loading thread (except on release, which shouldn't happen until the
|
||||
// loading thread has been terminated).
|
||||
private Sample pendingSample;
|
||||
|
||||
protected SampleQueue(SamplePool samplePool) {
|
||||
this.samplePool = samplePool;
|
||||
internalQueue = new ConcurrentLinkedQueue<Sample>();
|
||||
protected SampleQueue(BufferPool bufferPool) {
|
||||
rollingBuffer = new RollingSampleBuffer(bufferPool);
|
||||
sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
|
||||
needKeyframe = true;
|
||||
lastReadTimeUs = Long.MIN_VALUE;
|
||||
spliceOutTimeUs = Long.MIN_VALUE;
|
||||
largestParsedTimestampUs = Long.MIN_VALUE;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return peek() == null;
|
||||
public void release() {
|
||||
rollingBuffer.release();
|
||||
}
|
||||
|
||||
// Called by the consuming thread.
|
||||
|
||||
public long getLargestParsedTimestampUs() {
|
||||
return largestParsedTimestampUs;
|
||||
}
|
||||
|
|
@ -67,8 +72,8 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
|||
return mediaFormat;
|
||||
}
|
||||
|
||||
protected void setMediaFormat(MediaFormat mediaFormat) {
|
||||
this.mediaFormat = mediaFormat;
|
||||
public boolean isEmpty() {
|
||||
return !advanceToEligibleSample();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -80,153 +85,114 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
|||
* @param holder A {@link SampleHolder} into which the sample should be read.
|
||||
* @return True if a sample was read. False otherwise.
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
public boolean getSample(SampleHolder holder) {
|
||||
Sample sample = peek();
|
||||
if (sample == null) {
|
||||
boolean foundEligibleSample = advanceToEligibleSample();
|
||||
if (!foundEligibleSample) {
|
||||
return false;
|
||||
}
|
||||
// Write the sample into the holder.
|
||||
if (holder.data == null || holder.data.capacity() < sample.size) {
|
||||
holder.replaceBuffer(sample.size);
|
||||
}
|
||||
if (holder.data != null) {
|
||||
holder.data.put(sample.data, 0, sample.size);
|
||||
}
|
||||
holder.size = sample.size;
|
||||
holder.flags = sample.isKeyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
|
||||
holder.timeUs = sample.timeUs;
|
||||
// Pop and recycle the sample, and update state.
|
||||
rollingBuffer.readSample(holder);
|
||||
needKeyframe = false;
|
||||
lastReadTimeUs = sample.timeUs;
|
||||
internalQueue.poll();
|
||||
samplePool.recycle(sample);
|
||||
lastReadTimeUs = holder.timeUs;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns (but does not remove) the next sample in the queue.
|
||||
*
|
||||
* @return The next sample from the queue, or null if a sample isn't available.
|
||||
*/
|
||||
private Sample peek() {
|
||||
Sample head = internalQueue.peek();
|
||||
if (needKeyframe) {
|
||||
// Peeking discard of samples until we find a keyframe or run out of available samples.
|
||||
while (head != null && !head.isKeyframe) {
|
||||
samplePool.recycle(head);
|
||||
internalQueue.poll();
|
||||
head = internalQueue.peek();
|
||||
}
|
||||
}
|
||||
if (head == null) {
|
||||
return null;
|
||||
}
|
||||
if (spliceOutTimeUs != Long.MIN_VALUE && head.timeUs >= spliceOutTimeUs) {
|
||||
// The sample is later than the time this queue is spliced out.
|
||||
samplePool.recycle(head);
|
||||
internalQueue.poll();
|
||||
return null;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards samples from the queue up to the specified time.
|
||||
*
|
||||
* @param timeUs The time up to which samples should be discarded, in microseconds.
|
||||
*/
|
||||
public void discardUntil(long timeUs) {
|
||||
Sample head = peek();
|
||||
while (head != null && head.timeUs < timeUs) {
|
||||
samplePool.recycle(head);
|
||||
internalQueue.poll();
|
||||
head = internalQueue.peek();
|
||||
// We're discarding at least one sample, so any subsequent read will need to start at
|
||||
// a keyframe.
|
||||
while (rollingBuffer.peekSample(sampleInfoHolder) && sampleInfoHolder.timeUs < timeUs) {
|
||||
rollingBuffer.skipSample();
|
||||
// We're discarding one or more samples. A subsequent read will need to start at a keyframe.
|
||||
needKeyframe = true;
|
||||
}
|
||||
lastReadTimeUs = Long.MIN_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the queue.
|
||||
*/
|
||||
public final void release() {
|
||||
Sample toRecycle = internalQueue.poll();
|
||||
while (toRecycle != null) {
|
||||
samplePool.recycle(toRecycle);
|
||||
toRecycle = internalQueue.poll();
|
||||
}
|
||||
if (pendingSample != null) {
|
||||
samplePool.recycle(pendingSample);
|
||||
pendingSample = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
public boolean configureSpliceTo(SampleQueue nextQueue) {
|
||||
if (spliceOutTimeUs != Long.MIN_VALUE) {
|
||||
// We've already configured the splice.
|
||||
return true;
|
||||
}
|
||||
long firstPossibleSpliceTime;
|
||||
Sample nextSample = internalQueue.peek();
|
||||
if (nextSample != null) {
|
||||
firstPossibleSpliceTime = nextSample.timeUs;
|
||||
if (rollingBuffer.peekSample(sampleInfoHolder)) {
|
||||
firstPossibleSpliceTime = sampleInfoHolder.timeUs;
|
||||
} else {
|
||||
firstPossibleSpliceTime = lastReadTimeUs + 1;
|
||||
}
|
||||
Sample nextQueueSample = nextQueue.internalQueue.peek();
|
||||
while (nextQueueSample != null
|
||||
&& (nextQueueSample.timeUs < firstPossibleSpliceTime || !nextQueueSample.isKeyframe)) {
|
||||
RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer;
|
||||
while (nextRollingBuffer.peekSample(sampleInfoHolder)
|
||||
&& (sampleInfoHolder.timeUs < firstPossibleSpliceTime
|
||||
|| (sampleInfoHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) == 0)) {
|
||||
// Discard samples from the next queue for as long as they are before the earliest possible
|
||||
// splice time, or not keyframes.
|
||||
nextQueue.internalQueue.poll();
|
||||
nextQueueSample = nextQueue.internalQueue.peek();
|
||||
nextRollingBuffer.skipSample();
|
||||
}
|
||||
if (nextQueueSample != null) {
|
||||
if (nextRollingBuffer.peekSample(sampleInfoHolder)) {
|
||||
// We've found a keyframe in the next queue that can serve as the splice point. Set the
|
||||
// splice point now.
|
||||
spliceOutTimeUs = nextQueueSample.timeUs;
|
||||
spliceOutTimeUs = sampleInfoHolder.timeUs;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Writing side.
|
||||
|
||||
protected final boolean havePendingSample() {
|
||||
return pendingSample != null;
|
||||
}
|
||||
|
||||
protected final Sample getPendingSample() {
|
||||
return pendingSample;
|
||||
}
|
||||
|
||||
protected final void startSample(int type, long timeUs) {
|
||||
pendingSample = samplePool.get(type);
|
||||
pendingSample.timeUs = timeUs;
|
||||
}
|
||||
|
||||
protected final void appendSampleData(ParsableByteArray buffer, int size) {
|
||||
if (pendingSample.data.length - pendingSample.size < size) {
|
||||
pendingSample.expand(size - pendingSample.data.length + pendingSample.size);
|
||||
/**
|
||||
* Advances the underlying buffer to the next sample that is eligible to be returned.
|
||||
*
|
||||
* @boolean True if an eligible sample was found. False otherwise, in which case the underlying
|
||||
* buffer has been emptied.
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
private boolean advanceToEligibleSample() {
|
||||
boolean haveNext = rollingBuffer.peekSample(sampleInfoHolder);
|
||||
if (needKeyframe) {
|
||||
while (haveNext && (sampleInfoHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) == 0) {
|
||||
rollingBuffer.skipSample();
|
||||
haveNext = rollingBuffer.peekSample(sampleInfoHolder);
|
||||
}
|
||||
}
|
||||
buffer.readBytes(pendingSample.data, pendingSample.size, size);
|
||||
pendingSample.size += size;
|
||||
if (!haveNext) {
|
||||
return false;
|
||||
}
|
||||
if (spliceOutTimeUs != Long.MIN_VALUE && sampleInfoHolder.timeUs >= spliceOutTimeUs) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected final void commitSample(boolean isKeyframe) {
|
||||
pendingSample.isKeyframe = isKeyframe;
|
||||
internalQueue.add(pendingSample);
|
||||
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, pendingSample.timeUs);
|
||||
pendingSample = null;
|
||||
// Called by the loading thread.
|
||||
|
||||
protected boolean writingSample() {
|
||||
return writingSample;
|
||||
}
|
||||
|
||||
protected void setMediaFormat(MediaFormat mediaFormat) {
|
||||
this.mediaFormat = mediaFormat;
|
||||
}
|
||||
|
||||
protected void startSample(long sampleTimeUs) {
|
||||
writingSample = true;
|
||||
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs);
|
||||
rollingBuffer.startSample(sampleTimeUs);
|
||||
}
|
||||
|
||||
protected void appendSampleData(ParsableByteArray buffer, int size) {
|
||||
rollingBuffer.appendSampleData(buffer, size);
|
||||
}
|
||||
|
||||
protected void commitSample(boolean isKeyframe) {
|
||||
rollingBuffer.commitSample(isKeyframe);
|
||||
writingSample = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls.parser;
|
|||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.text.eia608.Eia608Parser;
|
||||
import com.google.android.exoplayer.upstream.BufferPool;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
/**
|
||||
|
|
@ -29,8 +30,8 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
|||
|
||||
private final ParsableByteArray seiBuffer;
|
||||
|
||||
public SeiReader(SamplePool samplePool) {
|
||||
super(samplePool);
|
||||
public SeiReader(BufferPool bufferPool) {
|
||||
super(bufferPool);
|
||||
setMediaFormat(MediaFormat.createEia608Format());
|
||||
seiBuffer = new ParsableByteArray();
|
||||
}
|
||||
|
|
@ -40,7 +41,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
|||
seiBuffer.setPosition(position + 4);
|
||||
int ccDataSize = Eia608Parser.parseHeader(seiBuffer);
|
||||
if (ccDataSize > 0) {
|
||||
startSample(Sample.TYPE_MISC, pesTimeUs);
|
||||
startSample(pesTimeUs);
|
||||
appendSampleData(seiBuffer, ccDataSize);
|
||||
commitSample(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer.hls.parser;
|
|||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.upstream.BufferPool;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.ParsableBitArray;
|
||||
|
|
@ -49,7 +50,7 @@ public final class TsExtractor {
|
|||
private final ParsableByteArray tsPacketBuffer;
|
||||
private final SparseArray<SampleQueue> sampleQueues; // Indexed by streamType
|
||||
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
|
||||
private final SamplePool samplePool;
|
||||
private final BufferPool bufferPool;
|
||||
private final boolean shouldSpliceIn;
|
||||
private final long firstSampleTimestamp;
|
||||
private final ParsableBitArray tsScratch;
|
||||
|
|
@ -65,10 +66,10 @@ public final class TsExtractor {
|
|||
// Accessed by both the loading and consuming threads.
|
||||
private volatile boolean prepared;
|
||||
|
||||
public TsExtractor(long firstSampleTimestamp, SamplePool samplePool, boolean shouldSpliceIn) {
|
||||
public TsExtractor(long firstSampleTimestamp, boolean shouldSpliceIn, BufferPool bufferPool) {
|
||||
this.firstSampleTimestamp = firstSampleTimestamp;
|
||||
this.samplePool = samplePool;
|
||||
this.shouldSpliceIn = shouldSpliceIn;
|
||||
this.bufferPool = bufferPool;
|
||||
tsScratch = new ParsableBitArray(new byte[3]);
|
||||
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
|
||||
sampleQueues = new SparseArray<SampleQueue>();
|
||||
|
|
@ -406,15 +407,15 @@ public final class TsExtractor {
|
|||
PesPayloadReader pesPayloadReader = null;
|
||||
switch (streamType) {
|
||||
case TS_STREAM_TYPE_AAC:
|
||||
pesPayloadReader = new AdtsReader(samplePool);
|
||||
pesPayloadReader = new AdtsReader(bufferPool);
|
||||
break;
|
||||
case TS_STREAM_TYPE_H264:
|
||||
SeiReader seiReader = new SeiReader(samplePool);
|
||||
SeiReader seiReader = new SeiReader(bufferPool);
|
||||
sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader);
|
||||
pesPayloadReader = new H264Reader(samplePool, seiReader);
|
||||
pesPayloadReader = new H264Reader(bufferPool, seiReader);
|
||||
break;
|
||||
case TS_STREAM_TYPE_ID3:
|
||||
pesPayloadReader = new Id3Reader(samplePool);
|
||||
pesPayloadReader = new Id3Reader(bufferPool);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,12 +96,38 @@ public final class BufferPool implements Allocator {
|
|||
allocatedBufferCount += requiredBufferCount - firstNewBufferIndex;
|
||||
for (int i = firstNewBufferIndex; i < requiredBufferCount; i++) {
|
||||
// Use a recycled buffer if one is available. Else instantiate a new one.
|
||||
buffers[i] = recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount] :
|
||||
new byte[bufferLength];
|
||||
buffers[i] = nextBuffer();
|
||||
}
|
||||
return buffers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a single buffer directly from the pool.
|
||||
* <p>
|
||||
* When the caller has finished with the buffer, it should be returned to the pool by calling
|
||||
* {@link #releaseDirect(byte[])}.
|
||||
*
|
||||
* @return The allocated buffer.
|
||||
*/
|
||||
public synchronized byte[] allocateDirect() {
|
||||
allocatedBufferCount++;
|
||||
return nextBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single buffer to the pool.
|
||||
*
|
||||
* @param buffer The buffer being returned.
|
||||
*/
|
||||
public synchronized void releaseDirect(byte[] buffer) {
|
||||
// Weak sanity check that the buffer probably originated from this pool.
|
||||
Assertions.checkArgument(buffer.length == bufferLength);
|
||||
allocatedBufferCount--;
|
||||
|
||||
ensureRecycledBufferCapacity(recycledBufferCount + 1);
|
||||
recycledBuffers[recycledBufferCount++] = buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the buffers belonging to an allocation to the pool.
|
||||
*
|
||||
|
|
@ -112,14 +138,7 @@ public final class BufferPool implements Allocator {
|
|||
allocatedBufferCount -= buffers.length;
|
||||
|
||||
int newRecycledBufferCount = recycledBufferCount + buffers.length;
|
||||
if (recycledBuffers.length < newRecycledBufferCount) {
|
||||
// Expand the capacity of the recycled buffers array.
|
||||
byte[][] newRecycledBuffers = new byte[newRecycledBufferCount * 2][];
|
||||
if (recycledBufferCount > 0) {
|
||||
System.arraycopy(recycledBuffers, 0, newRecycledBuffers, 0, recycledBufferCount);
|
||||
}
|
||||
recycledBuffers = newRecycledBuffers;
|
||||
}
|
||||
ensureRecycledBufferCapacity(newRecycledBufferCount);
|
||||
System.arraycopy(buffers, 0, recycledBuffers, recycledBufferCount, buffers.length);
|
||||
recycledBufferCount = newRecycledBufferCount;
|
||||
}
|
||||
|
|
@ -128,6 +147,22 @@ public final class BufferPool implements Allocator {
|
|||
return (int) ((size + bufferLength - 1) / bufferLength);
|
||||
}
|
||||
|
||||
private byte[] nextBuffer() {
|
||||
return recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount]
|
||||
: new byte[bufferLength];
|
||||
}
|
||||
|
||||
private void ensureRecycledBufferCapacity(int requiredCapacity) {
|
||||
if (recycledBuffers.length < requiredCapacity) {
|
||||
// Expand the capacity of the recycled buffers array.
|
||||
byte[][] newRecycledBuffers = new byte[requiredCapacity * 2][];
|
||||
if (recycledBufferCount > 0) {
|
||||
System.arraycopy(recycledBuffers, 0, newRecycledBuffers, 0, recycledBufferCount);
|
||||
}
|
||||
recycledBuffers = newRecycledBuffers;
|
||||
}
|
||||
}
|
||||
|
||||
private class AllocationImpl implements Allocation {
|
||||
|
||||
private byte[][] buffers;
|
||||
|
|
|
|||
Loading…
Reference in a new issue