diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index d7f5beef2d..ee9b7c6cca 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -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; } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java index 5e1b740684..07e649979c 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java @@ -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); diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java index 07b3e52d44..33ca516c4a 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java @@ -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); diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java index a579b47855..10229200d9 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java @@ -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()); } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java index 3c3f864276..2bdce8448a 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java @@ -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); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/RollingSampleBuffer.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/RollingSampleBuffer.java new file mode 100644 index 0000000000..032c7946d2 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/RollingSampleBuffer.java @@ -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 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(); + 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. + *

+ * 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. + *

+ * 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; + } + } + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/Sample.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/Sample.java deleted file mode 100644 index d3b145d1c8..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/Sample.java +++ /dev/null @@ -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; - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/SamplePool.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/SamplePool.java deleted file mode 100644 index 240c8508cc..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/SamplePool.java +++ /dev/null @@ -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; - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java index a4d418c716..d372b10f23 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java @@ -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 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(); + 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; } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java index a86b44a18d..de20e88a7e 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java @@ -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); } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java index 9c174755c1..d7ad5e7dde 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java @@ -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 sampleQueues; // Indexed by streamType private final SparseArray 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(); @@ -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; } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java b/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java index a7d847d5a1..af2ce03a20 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java @@ -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. + *

+ * 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;