Commit 37e6946c by Oliver Woodman

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
parent 28166d8c
......@@ -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;
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, pendingSample.timeUs);
}
position += 4;
// 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;
}
}
boolean isKeyframe = pendingSampleSize > idrNalUnitPosition;
if (!hasMediaFormat() && isKeyframe) {
parseMediaFormat(pendingSampleData, pendingSampleSize);
}
commitSample(isKeyframe);
// 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);
}
/**
......
/*
* 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;
}
}
/*
* 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.
/**
* 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);
}
}
if (!haveNext) {
return false;
}
if (spliceOutTimeUs != Long.MIN_VALUE && sampleInfoHolder.timeUs >= spliceOutTimeUs) {
return false;
}
return true;
}
// Called by the loading thread.
protected final boolean havePendingSample() {
return pendingSample != null;
protected boolean writingSample() {
return writingSample;
}
protected final Sample getPendingSample() {
return pendingSample;
protected void setMediaFormat(MediaFormat mediaFormat) {
this.mediaFormat = mediaFormat;
}
protected final void startSample(int type, long timeUs) {
pendingSample = samplePool.get(type);
pendingSample.timeUs = timeUs;
protected void startSample(long sampleTimeUs) {
writingSample = true;
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs);
rollingBuffer.startSample(sampleTimeUs);
}
protected final void appendSampleData(ParsableByteArray buffer, int size) {
if (pendingSample.data.length - pendingSample.size < size) {
pendingSample.expand(size - pendingSample.data.length + pendingSample.size);
}
buffer.readBytes(pendingSample.data, pendingSample.size, size);
pendingSample.size += size;
protected void appendSampleData(ParsableByteArray buffer, int size) {
rollingBuffer.appendSampleData(buffer, size);
}
protected final void commitSample(boolean isKeyframe) {
pendingSample.isKeyframe = isKeyframe;
internalQueue.add(pendingSample);
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, pendingSample.timeUs);
pendingSample = null;
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,13 +96,39 @@ 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.
*
* @param allocation The allocation to return.
......@@ -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;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment