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; ...@@ -17,10 +17,10 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; 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.hls.parser.TsExtractor;
import com.google.android.exoplayer.upstream.Aes128DataSource; import com.google.android.exoplayer.upstream.Aes128DataSource;
import com.google.android.exoplayer.upstream.BandwidthMeter; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException;
...@@ -102,7 +102,7 @@ public class HlsChunkSource { ...@@ -102,7 +102,7 @@ public class HlsChunkSource {
private static final String TAG = "HlsChunkSource"; private static final String TAG = "HlsChunkSource";
private static final float BANDWIDTH_FRACTION = 0.8f; private static final float BANDWIDTH_FRACTION = 0.8f;
private final SamplePool samplePool = new SamplePool(); private final BufferPool bufferPool;
private final DataSource upstreamDataSource; private final DataSource upstreamDataSource;
private final HlsPlaylistParser playlistParser; private final HlsPlaylistParser playlistParser;
private final Variant[] enabledVariants; private final Variant[] enabledVariants;
...@@ -165,6 +165,7 @@ public class HlsChunkSource { ...@@ -165,6 +165,7 @@ public class HlsChunkSource {
maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000; maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000;
baseUri = playlist.baseUri; baseUri = playlist.baseUri;
playlistParser = new HlsPlaylistParser(); playlistParser = new HlsPlaylistParser();
bufferPool = new BufferPool(256 * 1024);
if (playlist.type == HlsPlaylist.TYPE_MEDIA) { if (playlist.type == HlsPlaylist.TYPE_MEDIA) {
enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)}; enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)};
...@@ -324,7 +325,7 @@ public class HlsChunkSource { ...@@ -324,7 +325,7 @@ public class HlsChunkSource {
// Configure the extractor that will read the chunk. // Configure the extractor that will read the chunk.
TsExtractor extractor; TsExtractor extractor;
if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) { if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) {
extractor = new TsExtractor(startTimeUs, samplePool, switchingVariantSpliced); extractor = new TsExtractor(startTimeUs, switchingVariantSpliced, bufferPool);
} else { } else {
extractor = previousTsChunk.extractor; extractor = previousTsChunk.extractor;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls.parser; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; 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.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableBitArray;
...@@ -44,7 +45,7 @@ import java.util.Collections; ...@@ -44,7 +45,7 @@ import java.util.Collections;
private int bytesRead; private int bytesRead;
// Used to find the header. // Used to find the header.
private boolean lastByteWasOxFF; private boolean lastByteWasFF;
private boolean hasCrc; private boolean hasCrc;
// Parsed from the header. // Parsed from the header.
...@@ -54,8 +55,8 @@ import java.util.Collections; ...@@ -54,8 +55,8 @@ import java.util.Collections;
// Used when reading the samples. // Used when reading the samples.
private long timeUs; private long timeUs;
public AdtsReader(SamplePool samplePool) { public AdtsReader(BufferPool bufferPool) {
super(samplePool); super(bufferPool);
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
state = STATE_FINDING_SYNC; state = STATE_FINDING_SYNC;
} }
...@@ -77,7 +78,7 @@ import java.util.Collections; ...@@ -77,7 +78,7 @@ import java.util.Collections;
int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
if (continueRead(data, adtsScratch.getData(), targetLength)) { if (continueRead(data, adtsScratch.getData(), targetLength)) {
parseHeader(); parseHeader();
startSample(Sample.TYPE_AUDIO, timeUs); startSample(timeUs);
bytesRead = 0; bytesRead = 0;
state = STATE_READING_SAMPLE; state = STATE_READING_SAMPLE;
} }
...@@ -130,9 +131,9 @@ import java.util.Collections; ...@@ -130,9 +131,9 @@ import java.util.Collections;
int startOffset = pesBuffer.getPosition(); int startOffset = pesBuffer.getPosition();
int endOffset = pesBuffer.limit(); int endOffset = pesBuffer.limit();
for (int i = startOffset; i < endOffset; i++) { for (int i = startOffset; i < endOffset; i++) {
boolean byteIsOxFF = (adtsData[i] & 0xFF) == 0xFF; boolean byteIsFF = (adtsData[i] & 0xFF) == 0xFF;
boolean found = lastByteWasOxFF && !byteIsOxFF && (adtsData[i] & 0xF0) == 0xF0; boolean found = lastByteWasFF && !byteIsFF && (adtsData[i] & 0xF0) == 0xF0;
lastByteWasOxFF = byteIsOxFF; lastByteWasFF = byteIsFF;
if (found) { if (found) {
hasCrc = (adtsData[i] & 0x1) == 0; hasCrc = (adtsData[i] & 0x1) == 0;
pesBuffer.setPosition(i + 1); pesBuffer.setPosition(i + 1);
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls.parser; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.mp4.Mp4Util; 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.MimeTypes;
import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
...@@ -36,20 +37,27 @@ import java.util.List; ...@@ -36,20 +37,27 @@ import java.util.List;
private static final int NAL_UNIT_TYPE_AUD = 9; private static final int NAL_UNIT_TYPE_AUD = 9;
private final SeiReader seiReader; private final SeiReader seiReader;
private final ParsableByteArray pendingSampleWrapper;
public H264Reader(SamplePool samplePool, SeiReader seiReader) { // TODO: Ideally we wouldn't need to have a copy step through a byte array here.
super(samplePool); private byte[] pendingSampleData;
private int pendingSampleSize;
private long pendingSampleTimeUs;
public H264Reader(BufferPool bufferPool, SeiReader seiReader) {
super(bufferPool);
this.seiReader = seiReader; this.seiReader = seiReader;
this.pendingSampleData = new byte[1024];
this.pendingSampleWrapper = new ParsableByteArray();
} }
@Override @Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
if (readToNextAudUnit(data, pesTimeUs)) { boolean sampleFinished = readToNextAudUnit(data, pesTimeUs);
// TODO: Allowing access to the Sample object here is messy. Fix this. if (!sampleFinished) {
Sample pendingSample = getPendingSample(); continue;
byte[] pendingSampleData = pendingSample.data; }
int pendingSampleSize = pendingSample.size;
// Scan the sample to find relevant NAL units. // Scan the sample to find relevant NAL units.
int position = 0; int position = 0;
...@@ -61,20 +69,24 @@ import java.util.List; ...@@ -61,20 +69,24 @@ import java.util.List;
if (type == NAL_UNIT_TYPE_IDR) { if (type == NAL_UNIT_TYPE_IDR) {
idrNalUnitPosition = position; idrNalUnitPosition = position;
} else if (type == NAL_UNIT_TYPE_SEI) { } else if (type == NAL_UNIT_TYPE_SEI) {
seiReader.read(pendingSampleData, position, pendingSample.timeUs); seiReader.read(pendingSampleData, position, pendingSampleTimeUs);
} }
position += 4; position += 4;
} }
} }
// Determine whether the sample is a keyframe.
boolean isKeyframe = pendingSampleSize > idrNalUnitPosition; boolean isKeyframe = pendingSampleSize > idrNalUnitPosition;
if (!hasMediaFormat() && isKeyframe) { if (!hasMediaFormat() && isKeyframe) {
parseMediaFormat(pendingSampleData, pendingSampleSize); parseMediaFormat(pendingSampleData, pendingSampleSize);
} }
// Commit the sample to the queue.
pendingSampleWrapper.reset(pendingSampleData, pendingSampleSize);
appendSampleData(pendingSampleWrapper, pendingSampleSize);
commitSample(isKeyframe); commitSample(isKeyframe);
} }
} }
}
@Override @Override
public void packetFinished() { public void packetFinished() {
...@@ -97,15 +109,17 @@ import java.util.List; ...@@ -97,15 +109,17 @@ import java.util.List;
int audOffset = Mp4Util.findNalUnit(data.data, pesOffset, pesLimit, NAL_UNIT_TYPE_AUD); int audOffset = Mp4Util.findNalUnit(data.data, pesOffset, pesLimit, NAL_UNIT_TYPE_AUD);
int bytesToNextAud = audOffset - pesOffset; int bytesToNextAud = audOffset - pesOffset;
if (bytesToNextAud == 0) { if (bytesToNextAud == 0) {
if (!havePendingSample()) { if (!writingSample()) {
startSample(Sample.TYPE_VIDEO, pesTimeUs); startSample(pesTimeUs);
appendSampleData(data, 4); pendingSampleSize = 0;
pendingSampleTimeUs = pesTimeUs;
appendToSample(data, 4);
return false; return false;
} else { } else {
return true; return true;
} }
} else if (havePendingSample()) { } else if (writingSample()) {
appendSampleData(data, bytesToNextAud); appendToSample(data, bytesToNextAud);
return data.bytesLeft() > 0; return data.bytesLeft() > 0;
} else { } else {
data.skip(bytesToNextAud); data.skip(bytesToNextAud);
...@@ -113,6 +127,17 @@ import java.util.List; ...@@ -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) { private void parseMediaFormat(byte[] sampleData, int sampleSize) {
// Locate the SPS and PPS units. // Locate the SPS and PPS units.
int spsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_SPS); int spsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_SPS);
......
...@@ -16,27 +16,25 @@ ...@@ -16,27 +16,25 @@
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import android.annotation.SuppressLint;
/** /**
* Parses ID3 data and extracts individual text information frames. * Parses ID3 data and extracts individual text information frames.
*/ */
/* package */ class Id3Reader extends PesPayloadReader { /* package */ class Id3Reader extends PesPayloadReader {
public Id3Reader(SamplePool samplePool) { public Id3Reader(BufferPool bufferPool) {
super(samplePool); super(bufferPool);
setMediaFormat(MediaFormat.createId3Format()); setMediaFormat(MediaFormat.createId3Format());
} }
@SuppressLint("InlinedApi")
@Override @Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
if (startOfPacket) { if (startOfPacket) {
startSample(Sample.TYPE_MISC, pesTimeUs); startSample(pesTimeUs);
} }
if (havePendingSample()) { if (writingSample()) {
appendSampleData(data, data.bytesLeft()); appendSampleData(data, data.bytesLeft());
} }
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.hls.parser; package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
/** /**
...@@ -22,8 +23,8 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -22,8 +23,8 @@ import com.google.android.exoplayer.util.ParsableByteArray;
*/ */
/* package */ abstract class PesPayloadReader extends SampleQueue { /* package */ abstract class PesPayloadReader extends SampleQueue {
protected PesPayloadReader(SamplePool samplePool) { protected PesPayloadReader(BufferPool bufferPool) {
super(samplePool); 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; ...@@ -17,44 +17,49 @@ package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.media.MediaExtractor; 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 { /* package */ abstract class SampleQueue {
private final SamplePool samplePool; private final RollingSampleBuffer rollingBuffer;
private final ConcurrentLinkedQueue<Sample> internalQueue; private final SampleHolder sampleInfoHolder;
// Accessed only by the consuming thread. // Accessed only by the consuming thread.
private boolean needKeyframe; private boolean needKeyframe;
private long lastReadTimeUs; private long lastReadTimeUs;
private long spliceOutTimeUs; private long spliceOutTimeUs;
// Accessed only by the loading thread.
private boolean writingSample;
// Accessed by both the loading and consuming threads. // Accessed by both the loading and consuming threads.
private volatile MediaFormat mediaFormat; private volatile MediaFormat mediaFormat;
private volatile long largestParsedTimestampUs; private volatile long largestParsedTimestampUs;
// Accessed by only the loading thread (except on release, which shouldn't happen until the protected SampleQueue(BufferPool bufferPool) {
// loading thread has been terminated). rollingBuffer = new RollingSampleBuffer(bufferPool);
private Sample pendingSample; sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
protected SampleQueue(SamplePool samplePool) {
this.samplePool = samplePool;
internalQueue = new ConcurrentLinkedQueue<Sample>();
needKeyframe = true; needKeyframe = true;
lastReadTimeUs = Long.MIN_VALUE; lastReadTimeUs = Long.MIN_VALUE;
spliceOutTimeUs = Long.MIN_VALUE; spliceOutTimeUs = Long.MIN_VALUE;
largestParsedTimestampUs = Long.MIN_VALUE; largestParsedTimestampUs = Long.MIN_VALUE;
} }
public boolean isEmpty() { public void release() {
return peek() == null; rollingBuffer.release();
} }
// Called by the consuming thread.
public long getLargestParsedTimestampUs() { public long getLargestParsedTimestampUs() {
return largestParsedTimestampUs; return largestParsedTimestampUs;
} }
...@@ -67,8 +72,8 @@ import java.util.concurrent.ConcurrentLinkedQueue; ...@@ -67,8 +72,8 @@ import java.util.concurrent.ConcurrentLinkedQueue;
return mediaFormat; return mediaFormat;
} }
protected void setMediaFormat(MediaFormat mediaFormat) { public boolean isEmpty() {
this.mediaFormat = mediaFormat; return !advanceToEligibleSample();
} }
/** /**
...@@ -80,153 +85,114 @@ import java.util.concurrent.ConcurrentLinkedQueue; ...@@ -80,153 +85,114 @@ import java.util.concurrent.ConcurrentLinkedQueue;
* @param holder A {@link SampleHolder} into which the sample should be read. * @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise. * @return True if a sample was read. False otherwise.
*/ */
@SuppressLint("InlinedApi")
public boolean getSample(SampleHolder holder) { public boolean getSample(SampleHolder holder) {
Sample sample = peek(); boolean foundEligibleSample = advanceToEligibleSample();
if (sample == null) { if (!foundEligibleSample) {
return false; return false;
} }
// Write the sample into the holder. // Write the sample into the holder.
if (holder.data == null || holder.data.capacity() < sample.size) { rollingBuffer.readSample(holder);
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.
needKeyframe = false; needKeyframe = false;
lastReadTimeUs = sample.timeUs; lastReadTimeUs = holder.timeUs;
internalQueue.poll();
samplePool.recycle(sample);
return true; 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. * Discards samples from the queue up to the specified time.
* *
* @param timeUs The time up to which samples should be discarded, in microseconds. * @param timeUs The time up to which samples should be discarded, in microseconds.
*/ */
public void discardUntil(long timeUs) { public void discardUntil(long timeUs) {
Sample head = peek(); while (rollingBuffer.peekSample(sampleInfoHolder) && sampleInfoHolder.timeUs < timeUs) {
while (head != null && head.timeUs < timeUs) { rollingBuffer.skipSample();
samplePool.recycle(head); // We're discarding one or more samples. A subsequent read will need to start at a keyframe.
internalQueue.poll();
head = internalQueue.peek();
// We're discarding at least one sample, so any subsequent read will need to start at
// a keyframe.
needKeyframe = true; needKeyframe = true;
} }
lastReadTimeUs = Long.MIN_VALUE; 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. * Attempts to configure a splice from this queue to the next.
* *
* @param nextQueue The queue being spliced to. * @param nextQueue The queue being spliced to.
* @return Whether the splice was configured successfully. * @return Whether the splice was configured successfully.
*/ */
@SuppressLint("InlinedApi")
public boolean configureSpliceTo(SampleQueue nextQueue) { public boolean configureSpliceTo(SampleQueue nextQueue) {
if (spliceOutTimeUs != Long.MIN_VALUE) { if (spliceOutTimeUs != Long.MIN_VALUE) {
// We've already configured the splice. // We've already configured the splice.
return true; return true;
} }
long firstPossibleSpliceTime; long firstPossibleSpliceTime;
Sample nextSample = internalQueue.peek(); if (rollingBuffer.peekSample(sampleInfoHolder)) {
if (nextSample != null) { firstPossibleSpliceTime = sampleInfoHolder.timeUs;
firstPossibleSpliceTime = nextSample.timeUs;
} else { } else {
firstPossibleSpliceTime = lastReadTimeUs + 1; firstPossibleSpliceTime = lastReadTimeUs + 1;
} }
Sample nextQueueSample = nextQueue.internalQueue.peek(); RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer;
while (nextQueueSample != null while (nextRollingBuffer.peekSample(sampleInfoHolder)
&& (nextQueueSample.timeUs < firstPossibleSpliceTime || !nextQueueSample.isKeyframe)) { && (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 // Discard samples from the next queue for as long as they are before the earliest possible
// splice time, or not keyframes. // splice time, or not keyframes.
nextQueue.internalQueue.poll(); nextRollingBuffer.skipSample();
nextQueueSample = nextQueue.internalQueue.peek();
} }
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 // We've found a keyframe in the next queue that can serve as the splice point. Set the
// splice point now. // splice point now.
spliceOutTimeUs = nextQueueSample.timeUs; spliceOutTimeUs = sampleInfoHolder.timeUs;
return true; return true;
} }
return false; return false;
} }
// Writing side. /**
* Advances the underlying buffer to the next sample that is eligible to be returned.
protected final boolean havePendingSample() { *
return pendingSample != null; * @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 Sample getPendingSample() { protected boolean writingSample() {
return pendingSample; return writingSample;
} }
protected final void startSample(int type, long timeUs) { protected void setMediaFormat(MediaFormat mediaFormat) {
pendingSample = samplePool.get(type); this.mediaFormat = mediaFormat;
pendingSample.timeUs = timeUs;
} }
protected final void appendSampleData(ParsableByteArray buffer, int size) { protected void startSample(long sampleTimeUs) {
if (pendingSample.data.length - pendingSample.size < size) { writingSample = true;
pendingSample.expand(size - pendingSample.data.length + pendingSample.size); largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs);
rollingBuffer.startSample(sampleTimeUs);
} }
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) { protected void commitSample(boolean isKeyframe) {
pendingSample.isKeyframe = isKeyframe; rollingBuffer.commitSample(isKeyframe);
internalQueue.add(pendingSample); writingSample = false;
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, pendingSample.timeUs);
pendingSample = null;
} }
} }
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls.parser; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.text.eia608.Eia608Parser; import com.google.android.exoplayer.text.eia608.Eia608Parser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
/** /**
...@@ -29,8 +30,8 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -29,8 +30,8 @@ import com.google.android.exoplayer.util.ParsableByteArray;
private final ParsableByteArray seiBuffer; private final ParsableByteArray seiBuffer;
public SeiReader(SamplePool samplePool) { public SeiReader(BufferPool bufferPool) {
super(samplePool); super(bufferPool);
setMediaFormat(MediaFormat.createEia608Format()); setMediaFormat(MediaFormat.createEia608Format());
seiBuffer = new ParsableByteArray(); seiBuffer = new ParsableByteArray();
} }
...@@ -40,7 +41,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -40,7 +41,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
seiBuffer.setPosition(position + 4); seiBuffer.setPosition(position + 4);
int ccDataSize = Eia608Parser.parseHeader(seiBuffer); int ccDataSize = Eia608Parser.parseHeader(seiBuffer);
if (ccDataSize > 0) { if (ccDataSize > 0) {
startSample(Sample.TYPE_MISC, pesTimeUs); startSample(pesTimeUs);
appendSampleData(seiBuffer, ccDataSize); appendSampleData(seiBuffer, ccDataSize);
commitSample(true); commitSample(true);
} }
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer.hls.parser; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; 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.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableBitArray;
...@@ -49,7 +50,7 @@ public final class TsExtractor { ...@@ -49,7 +50,7 @@ public final class TsExtractor {
private final ParsableByteArray tsPacketBuffer; private final ParsableByteArray tsPacketBuffer;
private final SparseArray<SampleQueue> sampleQueues; // Indexed by streamType private final SparseArray<SampleQueue> sampleQueues; // Indexed by streamType
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final SamplePool samplePool; private final BufferPool bufferPool;
private final boolean shouldSpliceIn; private final boolean shouldSpliceIn;
private final long firstSampleTimestamp; private final long firstSampleTimestamp;
private final ParsableBitArray tsScratch; private final ParsableBitArray tsScratch;
...@@ -65,10 +66,10 @@ public final class TsExtractor { ...@@ -65,10 +66,10 @@ public final class TsExtractor {
// Accessed by both the loading and consuming threads. // Accessed by both the loading and consuming threads.
private volatile boolean prepared; private volatile boolean prepared;
public TsExtractor(long firstSampleTimestamp, SamplePool samplePool, boolean shouldSpliceIn) { public TsExtractor(long firstSampleTimestamp, boolean shouldSpliceIn, BufferPool bufferPool) {
this.firstSampleTimestamp = firstSampleTimestamp; this.firstSampleTimestamp = firstSampleTimestamp;
this.samplePool = samplePool;
this.shouldSpliceIn = shouldSpliceIn; this.shouldSpliceIn = shouldSpliceIn;
this.bufferPool = bufferPool;
tsScratch = new ParsableBitArray(new byte[3]); tsScratch = new ParsableBitArray(new byte[3]);
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
sampleQueues = new SparseArray<SampleQueue>(); sampleQueues = new SparseArray<SampleQueue>();
...@@ -406,15 +407,15 @@ public final class TsExtractor { ...@@ -406,15 +407,15 @@ public final class TsExtractor {
PesPayloadReader pesPayloadReader = null; PesPayloadReader pesPayloadReader = null;
switch (streamType) { switch (streamType) {
case TS_STREAM_TYPE_AAC: case TS_STREAM_TYPE_AAC:
pesPayloadReader = new AdtsReader(samplePool); pesPayloadReader = new AdtsReader(bufferPool);
break; break;
case TS_STREAM_TYPE_H264: case TS_STREAM_TYPE_H264:
SeiReader seiReader = new SeiReader(samplePool); SeiReader seiReader = new SeiReader(bufferPool);
sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader); sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader);
pesPayloadReader = new H264Reader(samplePool, seiReader); pesPayloadReader = new H264Reader(bufferPool, seiReader);
break; break;
case TS_STREAM_TYPE_ID3: case TS_STREAM_TYPE_ID3:
pesPayloadReader = new Id3Reader(samplePool); pesPayloadReader = new Id3Reader(bufferPool);
break; break;
} }
......
...@@ -96,13 +96,39 @@ public final class BufferPool implements Allocator { ...@@ -96,13 +96,39 @@ public final class BufferPool implements Allocator {
allocatedBufferCount += requiredBufferCount - firstNewBufferIndex; allocatedBufferCount += requiredBufferCount - firstNewBufferIndex;
for (int i = firstNewBufferIndex; i < requiredBufferCount; i++) { for (int i = firstNewBufferIndex; i < requiredBufferCount; i++) {
// Use a recycled buffer if one is available. Else instantiate a new one. // Use a recycled buffer if one is available. Else instantiate a new one.
buffers[i] = recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount] : buffers[i] = nextBuffer();
new byte[bufferLength];
} }
return buffers; 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. * Returns the buffers belonging to an allocation to the pool.
* *
* @param allocation The allocation to return. * @param allocation The allocation to return.
...@@ -112,14 +138,7 @@ public final class BufferPool implements Allocator { ...@@ -112,14 +138,7 @@ public final class BufferPool implements Allocator {
allocatedBufferCount -= buffers.length; allocatedBufferCount -= buffers.length;
int newRecycledBufferCount = recycledBufferCount + buffers.length; int newRecycledBufferCount = recycledBufferCount + buffers.length;
if (recycledBuffers.length < newRecycledBufferCount) { ensureRecycledBufferCapacity(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;
}
System.arraycopy(buffers, 0, recycledBuffers, recycledBufferCount, buffers.length); System.arraycopy(buffers, 0, recycledBuffers, recycledBufferCount, buffers.length);
recycledBufferCount = newRecycledBufferCount; recycledBufferCount = newRecycledBufferCount;
} }
...@@ -128,6 +147,22 @@ public final class BufferPool implements Allocator { ...@@ -128,6 +147,22 @@ public final class BufferPool implements Allocator {
return (int) ((size + bufferLength - 1) / bufferLength); 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 class AllocationImpl implements Allocation {
private byte[][] buffers; 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