From 7c66b6ed3b953adbe518203027d0ccba94e7cbc8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 10 Feb 2015 12:25:13 +0000 Subject: [PATCH 01/17] HLS optimization #1 (refactor). This is the start of a sequence of changes to fix the ref'd github issue. Currently TsExtractor involves multiple memory copy steps: DataSource->Ts_BitArray->Pes_BitArray->Sample->SampleHolder This is inefficient, but more importantly, the copy into Sample is problematic, because Samples are of dynamically varying size. The way we end up expanding Sample objects to be large enough to hold the data being written means that we end up gradually expanding all Sample objects in the pool (which wastes memory), and that we generate a lot of GC churn, particularly when switching to a higher quality which can trigger all Sample objects to expand. The fix will be to reduce the copy steps to: DataSource->TsPacket->SampleHolder We will track Pes and Sample data with lists of pointers into TsPackets, rather than actually copying the data. We will recycle these pointers. The following steps are approximately how the refactor will progress: 1. Start reducing use of BitArray. It's going to be way too complicated to track bit-granularity offsets into multiple packets, and allow reading across packet boundaries. In practice reads from Ts packets are all byte aligned except for small sections, so we'll move over to using ParsableByteArray instead, so we only need to track byte offsets. 2. Move TsExtractor to use ParsableByteArray except for small sections where we really need bit-granularity offsets. 3. Do the actual optimization. Issue: #278 --- demo/src/main/AndroidManifest.xml | 4 +- .../exoplayer/ExoPlayerLibraryInfo.java | 4 +- .../{BitArrayChunk.java => DataChunk.java} | 41 +++++++--- .../android/exoplayer/hls/HlsChunkSource.java | 30 ++++---- .../android/exoplayer/hls/TsExtractor.java | 75 +++++++++++++------ .../android/exoplayer/metadata/Id3Parser.java | 41 +++++----- .../google/android/exoplayer/mp4/Mp4Util.java | 33 ++++++++ .../android/exoplayer/util/BitArray.java | 60 ++------------- .../exoplayer/util/ParsableByteArray.java | 27 ++++++- 9 files changed, 185 insertions(+), 130 deletions(-) rename library/src/main/java/com/google/android/exoplayer/hls/{BitArrayChunk.java => DataChunk.java} (68%) diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 88b5eb5a93..596cd5cacc 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java index 6fb7d20be8..67b902aff9 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java @@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - public static final String VERSION = "1.1.0"; + public static final String VERSION = "1.2.0"; /** * The version of the library, expressed as an integer. @@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo { * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * corresponding integer version 001002003. */ - public static final int VERSION_INT = 001001000; + public static final int VERSION_INT = 001002000; /** * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/DataChunk.java similarity index 68% rename from library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java rename to library/src/main/java/com/google/android/exoplayer/hls/DataChunk.java index cc56691441..04d1bc2728 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/DataChunk.java @@ -17,19 +17,21 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; -import com.google.android.exoplayer.util.BitArray; import java.io.IOException; +import java.util.Arrays; /** * An abstract base class for {@link HlsChunk} implementations where the data should be loaded into - * a {@link BitArray} and subsequently consumed. + * a {@code byte[]} before being consumed. */ -public abstract class BitArrayChunk extends HlsChunk { +public abstract class DataChunk extends HlsChunk { private static final int READ_GRANULARITY = 16 * 1024; - private final BitArray bitArray; + private byte[] data; + private int limit; + private volatile boolean loadFinished; private volatile boolean loadCanceled; @@ -39,26 +41,27 @@ public abstract class BitArrayChunk extends HlsChunk { * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then * the length resolved by {@code dataSource.open(dataSpec)} must not exceed * {@link Integer#MAX_VALUE}. - * @param bitArray The {@link BitArray} into which the data should be loaded. + * @param data An optional recycled array that can be used as a holder for the data. */ - public BitArrayChunk(DataSource dataSource, DataSpec dataSpec, BitArray bitArray) { + public DataChunk(DataSource dataSource, DataSpec dataSpec, byte[] data) { super(dataSource, dataSpec); - this.bitArray = bitArray; + this.data = data; } @Override public void consume() throws IOException { - consume(bitArray); + consume(data, limit); } /** * Invoked by {@link #consume()}. Implementations should override this method to consume the * loaded data. * - * @param bitArray The {@link BitArray} containing the loaded data. + * @param data An array containing the data. + * @param limit The limit of the data. * @throws IOException If an error occurs consuming the loaded data. */ - protected abstract void consume(BitArray bitArray) throws IOException; + protected abstract void consume(byte[] data, int limit) throws IOException; /** * Whether the whole of the chunk has been loaded. @@ -85,11 +88,15 @@ public abstract class BitArrayChunk extends HlsChunk { @Override public final void load() throws IOException, InterruptedException { try { - bitArray.reset(); dataSource.open(dataSpec); + limit = 0; int bytesRead = 0; while (bytesRead != -1 && !loadCanceled) { - bytesRead = bitArray.append(dataSource, READ_GRANULARITY); + maybeExpandData(); + bytesRead = dataSource.read(data, limit, READ_GRANULARITY); + if (bytesRead != -1) { + limit += bytesRead; + } } loadFinished = !loadCanceled; } finally { @@ -97,4 +104,14 @@ public abstract class BitArrayChunk extends HlsChunk { } } + private void maybeExpandData() { + if (data == null) { + data = new byte[READ_GRANULARITY]; + } else if (data.length < limit + READ_GRANULARITY) { + // The new length is calculated as (data.length + READ_GRANULARITY) rather than + // (limit + READ_GRANULARITY) in order to avoid small increments in the length. + data = Arrays.copyOf(data, data.length + READ_GRANULARITY); + } + } + } 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 584372c86a..5f8ccaef89 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 @@ -24,7 +24,6 @@ import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -35,6 +34,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigInteger; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -106,7 +106,6 @@ public class HlsChunkSource { private final HlsPlaylistParser playlistParser; private final Variant[] enabledVariants; private final BandwidthMeter bandwidthMeter; - private final BitArray bitArray; private final int adaptiveMode; private final Uri baseUri; private final int maxWidth; @@ -115,6 +114,7 @@ public class HlsChunkSource { private final long minBufferDurationToSwitchUpUs; private final long maxBufferDurationToSwitchDownUs; + /* package */ byte[] scratchSpace; /* package */ final HlsMediaPlaylist[] mediaPlaylists; /* package */ final boolean[] mediaPlaylistBlacklistFlags; /* package */ final long[] lastMediaPlaylistLoadTimesMs; @@ -163,7 +163,6 @@ public class HlsChunkSource { minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000; maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000; baseUri = playlist.baseUri; - bitArray = new BitArray(); playlistParser = new HlsPlaylistParser(); if (playlist.type == HlsPlaylist.TYPE_MEDIA) { @@ -526,7 +525,7 @@ public class HlsChunkSource { return true; } - private class MediaPlaylistChunk extends BitArrayChunk { + private class MediaPlaylistChunk extends DataChunk { @SuppressWarnings("hiding") /* package */ final int variantIndex; @@ -535,37 +534,38 @@ public class HlsChunkSource { public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec, Uri playlistBaseUri) { - super(dataSource, dataSpec, bitArray); + super(dataSource, dataSpec, scratchSpace); this.variantIndex = variantIndex; this.playlistBaseUri = playlistBaseUri; } @Override - protected void consume(BitArray data) throws IOException { - HlsPlaylist playlist = playlistParser.parse( - new ByteArrayInputStream(data.getData(), 0, data.bytesLeft()), null, null, - playlistBaseUri); + protected void consume(byte[] data, int limit) throws IOException { + HlsPlaylist playlist = playlistParser.parse(new ByteArrayInputStream(data, 0, limit), + null, null, playlistBaseUri); Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MEDIA); HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; setMediaPlaylist(variantIndex, mediaPlaylist); + // Recycle the allocation. + scratchSpace = data; } } - private class EncryptionKeyChunk extends BitArrayChunk { + private class EncryptionKeyChunk extends DataChunk { private final String iv; public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) { - super(dataSource, dataSpec, bitArray); + super(dataSource, dataSpec, scratchSpace); this.iv = iv; } @Override - protected void consume(BitArray data) throws IOException { - byte[] secretKey = new byte[data.bytesLeft()]; - data.readBytes(secretKey, 0, secretKey.length); - initEncryptedDataSource(dataSpec.uri, iv, secretKey); + protected void consume(byte[] data, int limit) throws IOException { + initEncryptedDataSource(dataSpec.uri, iv, Arrays.copyOf(data, limit)); + // Recycle the allocation. + scratchSpace = data; } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index 5452391e24..cabfa2afed 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.mp4.Mp4Util; import com.google.android.exoplayer.text.eia608.Eia608Parser; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.Assertions; @@ -824,43 +825,49 @@ public final class TsExtractor { @SuppressLint("InlinedApi") private int readOneH264Frame(BitArray pesBuffer, boolean remainderOnly) { - int offset = remainderOnly ? 0 : 3; - int audStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_AUD, offset); + byte[] pesData = pesBuffer.getData(); + int pesOffset = pesBuffer.getByteOffset(); + int pesLimit = pesBuffer.limit(); + + int searchOffset = pesOffset + (remainderOnly ? 0 : 3); + int audOffset = Mp4Util.findNalUnit(pesData, searchOffset, pesLimit, NAL_UNIT_TYPE_AUD); + int bytesToNextAud = audOffset - pesOffset; if (currentSample != null) { - int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset); - if (idrStart < audStart) { + int idrOffset = Mp4Util.findNalUnit(pesData, searchOffset, pesLimit, NAL_UNIT_TYPE_IDR); + if (idrOffset < audOffset) { currentSample.isKeyframe = true; } - addToSample(currentSample, pesBuffer, audStart); + addToSample(currentSample, pesBuffer, bytesToNextAud); } else { - pesBuffer.skipBytes(audStart); + pesBuffer.skipBytes(bytesToNextAud); } - return audStart; + return bytesToNextAud; } private void parseMediaFormat(Sample sample) { - BitArray bitArray = new BitArray(sample.data, sample.size); + byte[] sampleData = sample.data; + int sampleSize = sample.size; // Locate the SPS and PPS units. - int spsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_SPS, 0); - int ppsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_PPS, 0); - if (spsOffset == bitArray.bytesLeft() || ppsOffset == bitArray.bytesLeft()) { + int spsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_SPS); + int ppsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_PPS); + if (spsOffset == sampleSize || ppsOffset == sampleSize) { return; } - int spsLength = bitArray.findNextNalUnit(-1, spsOffset + 3) - spsOffset; - int ppsLength = bitArray.findNextNalUnit(-1, ppsOffset + 3) - ppsOffset; + // Determine the length of the units, and copy them to build the initialization data. + int spsLength = Mp4Util.findNextNalUnit(sampleData, spsOffset + 3, sampleSize) - spsOffset; + int ppsLength = Mp4Util.findNextNalUnit(sampleData, ppsOffset + 3, sampleSize) - ppsOffset; byte[] spsData = new byte[spsLength]; byte[] ppsData = new byte[ppsLength]; - System.arraycopy(bitArray.getData(), spsOffset, spsData, 0, spsLength); - System.arraycopy(bitArray.getData(), ppsOffset, ppsData, 0, ppsLength); + System.arraycopy(sampleData, spsOffset, spsData, 0, spsLength); + System.arraycopy(sampleData, ppsOffset, ppsData, 0, ppsLength); List initializationData = new ArrayList(); initializationData.add(spsData); initializationData.add(ppsData); - // Unescape the SPS unit. + // Unescape and then parse the SPS unit. byte[] unescapedSps = unescapeStream(spsData, 0, spsLength); - bitArray.reset(unescapedSps, unescapedSps.length); + BitArray bitArray = new BitArray(unescapedSps, unescapedSps.length); - // Parse the SPS unit // Skip the NAL header. bitArray.skipBytes(4); int profileIdc = bitArray.readBits(8); @@ -1033,14 +1040,15 @@ public final class TsExtractor { } @SuppressLint("InlinedApi") - public void read(byte[] data, int size, long pesTimeUs) { - seiBuffer.reset(data, size); + public void read(byte[] data, int length, long pesTimeUs) { + seiBuffer.reset(data, length); while (seiBuffer.bytesLeft() > 0) { - int seiStart = seiBuffer.findNextNalUnit(NAL_UNIT_TYPE_SEI, 0); - if (seiStart == seiBuffer.bytesLeft()) { + int currentOffset = seiBuffer.getByteOffset(); + int seiOffset = Mp4Util.findNalUnit(data, currentOffset, length, NAL_UNIT_TYPE_SEI); + if (seiOffset == length) { return; } - seiBuffer.skipBytes(seiStart + 4); + seiBuffer.skipBytes(seiOffset + 4 - currentOffset); int ccDataSize = Eia608Parser.parseHeader(seiBuffer); if (ccDataSize > 0) { addSample(Sample.TYPE_MISC, seiBuffer, ccDataSize, pesTimeUs, true); @@ -1105,7 +1113,7 @@ public final class TsExtractor { return false; } - int offsetToSyncWord = adtsBuffer.findNextAdtsSyncWord(); + int offsetToSyncWord = findOffsetToSyncWord(); adtsBuffer.skipBytes(offsetToSyncWord); int adtsStartOffset = adtsBuffer.getByteOffset(); @@ -1168,6 +1176,25 @@ public final class TsExtractor { adtsBuffer.reset(); } + /** + * Finds the offset to the next Adts sync word. + * + * @return The position of the next Adts sync word. If an Adts sync word is not found, then the + * position of the end of the data is returned. + */ + private int findOffsetToSyncWord() { + byte[] adtsData = adtsBuffer.getData(); + int startOffset = adtsBuffer.getByteOffset(); + int endOffset = startOffset + adtsBuffer.bytesLeft(); + for (int i = startOffset; i < endOffset - 1; i++) { + int syncBits = ((adtsData[i] & 0xFF) << 8) | (adtsData[i + 1] & 0xFF); + if ((syncBits & 0xFFF0) == 0xFFF0 && syncBits != 0xFFFF) { + return i - startOffset; + } + } + return endOffset - startOffset; + } + } /** diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index efa3b66147..0b28614465 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer.metadata; import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableByteArray; import java.io.UnsupportedEncodingException; import java.util.Collections; @@ -37,30 +37,28 @@ public class Id3Parser implements MetadataParser> { @Override public Map parse(byte[] data, int size) throws UnsupportedEncodingException, ParserException { - BitArray id3Buffer = new BitArray(data, size); - int id3Size = parseId3Header(id3Buffer); - Map metadata = new HashMap(); + ParsableByteArray id3Data = new ParsableByteArray(data); + int id3Size = parseId3Header(id3Data); while (id3Size > 0) { - int frameId0 = id3Buffer.readUnsignedByte(); - int frameId1 = id3Buffer.readUnsignedByte(); - int frameId2 = id3Buffer.readUnsignedByte(); - int frameId3 = id3Buffer.readUnsignedByte(); - - int frameSize = id3Buffer.readSynchSafeInt(); + int frameId0 = id3Data.readUnsignedByte(); + int frameId1 = id3Data.readUnsignedByte(); + int frameId2 = id3Data.readUnsignedByte(); + int frameId3 = id3Data.readUnsignedByte(); + int frameSize = id3Data.readSynchSafeInt(); if (frameSize <= 1) { break; } - id3Buffer.skipBytes(2); // Skip frame flags. - + // Skip frame flags. + id3Data.skip(2); // Check Frame ID == TXXX. if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { - int encoding = id3Buffer.readUnsignedByte(); + int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); byte[] frame = new byte[frameSize - 1]; - id3Buffer.readBytes(frame, 0, frameSize - 1); + id3Data.readBytes(frame, 0, frameSize - 1); int firstZeroIndex = indexOf(frame, 0, (byte) 0); String description = new String(frame, 0, firstZeroIndex, charset); @@ -72,7 +70,7 @@ public class Id3Parser implements MetadataParser> { } else { String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3); byte[] frame = new byte[frameSize]; - id3Buffer.readBytes(frame, 0, frameSize); + id3Data.readBytes(frame, 0, frameSize); metadata.put(type, frame); } @@ -101,12 +99,13 @@ public class Id3Parser implements MetadataParser> { } /** - * Parses ID3 header. - * @param id3Buffer A {@link BitArray} with raw ID3 data. - * @return The size of data that contains ID3 frames without header and footer. + * Parses an ID3 header. + * + * @param id3Buffer A {@link ParsableByteArray} from which data should be read. + * @return The size of ID3 frames in bytes, excluding the header and footer. * @throws ParserException If ID3 file identifier != "ID3". */ - private static int parseId3Header(BitArray id3Buffer) throws ParserException { + private static int parseId3Header(ParsableByteArray id3Buffer) throws ParserException { int id1 = id3Buffer.readUnsignedByte(); int id2 = id3Buffer.readUnsignedByte(); int id3 = id3Buffer.readUnsignedByte(); @@ -114,7 +113,7 @@ public class Id3Parser implements MetadataParser> { throw new ParserException(String.format( "Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); } - id3Buffer.skipBytes(2); // Skip version. + id3Buffer.skip(2); // Skip version. int flags = id3Buffer.readUnsignedByte(); int id3Size = id3Buffer.readSynchSafeInt(); @@ -123,7 +122,7 @@ public class Id3Parser implements MetadataParser> { if ((flags & 0x2) != 0) { int extendedHeaderSize = id3Buffer.readSynchSafeInt(); if (extendedHeaderSize > 4) { - id3Buffer.skipBytes(extendedHeaderSize - 4); + id3Buffer.skip(extendedHeaderSize - 4); } id3Size -= extendedHeaderSize; } diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java index a78c9fb414..f5a8726a85 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java @@ -99,4 +99,37 @@ public final class Mp4Util { return CodecSpecificDataUtil.buildNalUnit(atom.data, offset, length); } + /** + * Like {@link #findNalUnit(byte[], int, int, int)} with {@code type == -1}. + * + * @param startOffset The offset (inclusive) in the data to start the search. + * @param endOffset The offset (exclusive) in the data to end the search. + * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. + */ + public static int findNextNalUnit(byte[] data, int startOffset, int endOffset) { + return findNalUnit(data, startOffset, endOffset, -1); + } + + /** + * Finds the first NAL unit in {@code data}. + *

+ * For a NAL unit to be found, its first four bytes must be contained within the part of the + * array being searched. + * + * @param type The type of the NAL unit to search for, or -1 for any NAL unit. + * @param startOffset The offset (inclusive) in the data to start the search. + * @param endOffset The offset (exclusive) in the data to end the search. + * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. + */ + public static int findNalUnit(byte[] data, int startOffset, int endOffset, int type) { + for (int i = startOffset; i < endOffset - 3; i++) { + // Check for NAL unit start code prefix == 0x000001. + if ((data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1) + && (type == -1 || (type == (data[i + 3] & 0x1F)))) { + return i; + } + } + return endOffset; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java index 45d7ec35d0..4a57a0b2f9 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java @@ -271,6 +271,13 @@ public final class BitArray { return limit - byteOffset; } + /** + * @return The limit of the data, specified in whole bytes. + */ + public int limit() { + return limit; + } + /** * @return Whether or not there is any data available. */ @@ -305,57 +312,4 @@ public final class BitArray { return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0); } - /** - * Reads a Synchsafe integer. - * Synchsafe integers are integers that keep the highest bit of every byte zeroed. - * A 32 bit synchsafe integer can store 28 bits of information. - * - * @return The value of the parsed Synchsafe integer. - */ - public int readSynchSafeInt() { - int b1 = readUnsignedByte(); - int b2 = readUnsignedByte(); - int b3 = readUnsignedByte(); - int b4 = readUnsignedByte(); - - return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4; - } - - // TODO: Find a better place for this method. - /** - * Finds the next Adts sync word. - * - * @return The offset from the current position to the start of the next Adts sync word. If an - * Adts sync word is not found, then the offset to the end of the data is returned. - */ - public int findNextAdtsSyncWord() { - for (int i = byteOffset; i < limit - 1; i++) { - int syncBits = (getUnsignedByte(i) << 8) | getUnsignedByte(i + 1); - if ((syncBits & 0xFFF0) == 0xFFF0 && syncBits != 0xFFFF) { - return i - byteOffset; - } - } - return limit - byteOffset; - } - - //TODO: Find a better place for this method. - /** - * Finds the next NAL unit. - * - * @param nalUnitType The type of the NAL unit to search for, or -1 for any NAL unit. - * @param offset The additional offset in the data to start the search from. - * @return The offset from the current position to the start of the NAL unit. If a NAL unit is - * not found, then the offset to the end of the data is returned. - */ - public int findNextNalUnit(int nalUnitType, int offset) { - for (int i = byteOffset + offset; i < limit - 3; i++) { - // Check for NAL unit start code prefix == 0x000001. - if ((data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1) - && (nalUnitType == -1 || (nalUnitType == (data[i + 3] & 0x1F)))) { - return i - byteOffset; - } - } - return limit - byteOffset; - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java index 9ea78dad13..c516c0c926 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java @@ -27,11 +27,20 @@ public final class ParsableByteArray { private int position; - /** Creates a new parsable array with {@code length} bytes. */ + /** Creates a new instance with {@code length} bytes. */ public ParsableByteArray(int length) { this.data = new byte[length]; } + /** + * Creates a new instance that wraps an existing array. + * + * @param data The data to wrap. + */ + public ParsableByteArray(byte[] data) { + this.data = data; + } + /** Returns the number of bytes in the array. */ public int length() { return data.length; @@ -127,6 +136,22 @@ public final class ParsableByteArray { return result; } + /** + * Reads a Synchsafe integer. + *

+ * Synchsafe integers keep the highest bit of every byte zeroed. A 32 bit synchsafe integer can + * store 28 bits of information. + * + * @return The parsed value. + */ + public int readSynchSafeInt() { + int b1 = readUnsignedByte(); + int b2 = readUnsignedByte(); + int b3 = readUnsignedByte(); + int b4 = readUnsignedByte(); + return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4; + } + /** * Reads the next four bytes as an unsigned integer into an integer, if the top bit is a zero. * From 797fa7f872a3e61d20e9dabd486078f889795793 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 10 Feb 2015 22:13:01 +0000 Subject: [PATCH 02/17] Make TsExtractor use ParsableByteArray where possible. - TsExtractor is now based on ParsableByteArray rather than BitArray. This makes is much clearer that, for the most part, data is byte aligned. It will allow us to optimize TsExtractor without worrying about arbitrary bit offsets. - BitArray is renamed ParsableBitArray for consistency, and is now exclusively for bit-stream level reading. - There are some temporary methods in ParsableByteArray that should be cleared up once the optimizations are in place. Issue: #278 --- .../android/exoplayer/hls/TsExtractor.java | 311 +++++++++--------- .../android/exoplayer/metadata/Id3Parser.java | 2 +- .../google/android/exoplayer/mp4/Mp4Util.java | 2 +- .../exoplayer/text/eia608/Eia608Parser.java | 17 +- .../{BitArray.java => ParsableBitArray.java} | 210 +++--------- .../exoplayer/util/ParsableByteArray.java | 80 ++++- 6 files changed, 291 insertions(+), 331 deletions(-) rename library/src/main/java/com/google/android/exoplayer/util/{BitArray.java => ParsableBitArray.java} (51%) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index cabfa2afed..9e43da5324 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -22,9 +22,10 @@ import com.google.android.exoplayer.mp4.Mp4Util; import com.google.android.exoplayer.text.eia608.Eia608Parser; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; import android.annotation.SuppressLint; import android.media.MediaExtractor; @@ -58,17 +59,19 @@ public final class TsExtractor { private static final long MAX_PTS = 0x1FFFFFFFFL; - private final BitArray tsPacketBuffer; + private final ParsableByteArray tsPacketBuffer; private final SparseArray sampleQueues; // Indexed by streamType private final SparseArray tsPayloadReaders; // Indexed by pid private final SamplePool samplePool; private final boolean shouldSpliceIn; private final long firstSampleTimestamp; + /* package */ final ParsableBitArray scratch; // Accessed only by the consuming thread. private boolean spliceConfigured; // Accessed only by the loading thread. + private int tsPacketBytesRead; private long timestampOffsetUs; private long lastPts; @@ -80,7 +83,8 @@ public final class TsExtractor { this.firstSampleTimestamp = firstSampleTimestamp; this.samplePool = samplePool; this.shouldSpliceIn = shouldSpliceIn; - tsPacketBuffer = new BitArray(); + scratch = new ParsableBitArray(new byte[5]); + tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); sampleQueues = new SparseArray(); tsPayloadReaders = new SparseArray(); tsPayloadReaders.put(TS_PAT_PID, new PatReader()); @@ -235,42 +239,44 @@ public final class TsExtractor { * @return The number of bytes read from the source. */ public int read(DataSource dataSource) throws IOException { - int read = tsPacketBuffer.append(dataSource, TS_PACKET_SIZE - tsPacketBuffer.bytesLeft()); - if (read == -1) { + int bytesRead = dataSource.read(tsPacketBuffer.data, tsPacketBytesRead, + TS_PACKET_SIZE - tsPacketBytesRead); + if (bytesRead == -1) { return -1; } - if (tsPacketBuffer.bytesLeft() != TS_PACKET_SIZE) { - return read; + tsPacketBytesRead += bytesRead; + if (tsPacketBytesRead < TS_PACKET_SIZE) { + // We haven't read the whole packet yet. + return bytesRead; } - // Parse TS header. - // Check sync byte. + // Reset before reading the packet. + tsPacketBytesRead = 0; + tsPacketBuffer.setPosition(0); + int syncByte = tsPacketBuffer.readUnsignedByte(); if (syncByte != TS_SYNC_BYTE) { - return read; + return bytesRead; } - // Skip transportErrorIndicator. - tsPacketBuffer.skipBits(1); - boolean payloadUnitStartIndicator = tsPacketBuffer.readBit(); - // Skip transportPriority. - tsPacketBuffer.skipBits(1); - int pid = tsPacketBuffer.readBits(13); - // Skip transport_scrambling_control. - tsPacketBuffer.skipBits(2); - boolean adaptationFieldExists = tsPacketBuffer.readBit(); - boolean payloadExists = tsPacketBuffer.readBit(); - // Skip continuityCounter. - tsPacketBuffer.skipBits(4); + tsPacketBuffer.readBytes(scratch, 3); + scratch.skipBits(1); // transport_error_indicator + boolean payloadUnitStartIndicator = scratch.readBit(); + scratch.skipBits(1); // transport_priority + int pid = scratch.readBits(13); + scratch.skipBits(2); // transport_scrambling_control + boolean adaptationFieldExists = scratch.readBit(); + boolean payloadExists = scratch.readBit(); + // Last 4 bits of scratch are skipped: continuity_counter - // Read the adaptation field. + // Skip the adaptation field. if (adaptationFieldExists) { - int adaptationFieldLength = tsPacketBuffer.readBits(8); - tsPacketBuffer.skipBytes(adaptationFieldLength); + int adaptationFieldLength = tsPacketBuffer.readUnsignedByte(); + tsPacketBuffer.skip(adaptationFieldLength); } - // Read Payload. + // Read the payload. if (payloadExists) { TsPayloadReader payloadReader = tsPayloadReaders.get(pid); if (payloadReader != null) { @@ -282,8 +288,7 @@ public final class TsExtractor { prepared = checkPrepared(); } - tsPacketBuffer.reset(); - return read; + return bytesRead; } @SuppressLint("InlinedApi") @@ -331,7 +336,7 @@ public final class TsExtractor { */ private abstract static class TsPayloadReader { - public abstract void read(BitArray tsBuffer, boolean payloadUnitStartIndicator); + public abstract void read(ParsableByteArray tsBuffer, boolean payloadUnitStartIndicator); } @@ -341,21 +346,25 @@ public final class TsExtractor { private class PatReader extends TsPayloadReader { @Override - public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { + public void read(ParsableByteArray tsBuffer, boolean payloadUnitStartIndicator) { // Skip pointer. if (payloadUnitStartIndicator) { - int pointerField = tsBuffer.readBits(8); - tsBuffer.skipBytes(pointerField); + int pointerField = tsBuffer.readUnsignedByte(); + tsBuffer.skip(pointerField); } - tsBuffer.skipBits(12); // 8+1+1+2 - int sectionLength = tsBuffer.readBits(12); - tsBuffer.skipBits(40); // 16+2+5+1+8+8 + tsBuffer.readBytes(scratch, 3); + scratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) + int sectionLength = scratch.readBits(12); + // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1), + // section_number (8), last_section_number (8) + tsBuffer.skip(5); int programCount = (sectionLength - 9) / 4; for (int i = 0; i < programCount; i++) { - tsBuffer.skipBits(19); - int pid = tsBuffer.readBits(13); + tsBuffer.readBytes(scratch, 4); + scratch.skipBits(19); // program_number (16), reserved (3) + int pid = scratch.readBits(13); tsPayloadReaders.put(pid, new PmtReader()); } @@ -370,34 +379,41 @@ public final class TsExtractor { private class PmtReader extends TsPayloadReader { @Override - public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { + public void read(ParsableByteArray tsBuffer, boolean payloadUnitStartIndicator) { // Skip pointer. if (payloadUnitStartIndicator) { - int pointerField = tsBuffer.readBits(8); - tsBuffer.skipBytes(pointerField); + int pointerField = tsBuffer.readUnsignedByte(); + tsBuffer.skip(pointerField); } - // Skip table_id, section_syntax_indicator, etc. - tsBuffer.skipBits(12); // 8+1+1+2 - int sectionLength = tsBuffer.readBits(12); + tsBuffer.readBytes(scratch, 3); + scratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) + int sectionLength = scratch.readBits(12); + + // program_number (16), reserved (2), version_number (5), current_next_indicator (1), + // section_number (8), last_section_number (8), reserved (3), PCR_PID (13) // Skip the rest of the PMT header. - tsBuffer.skipBits(60); // 16+2+5+1+8+8+3+13+4 + tsBuffer.skip(7); + + tsBuffer.readBytes(scratch, 2); + scratch.skipBits(4); + int programInfoLength = scratch.readBits(12); - int programInfoLength = tsBuffer.readBits(12); // Skip the descriptors. - tsBuffer.skipBytes(programInfoLength); + tsBuffer.skip(programInfoLength); - int entriesSize = sectionLength - 9 /* size of the rest of the fields before descriptors */ + int entriesSize = sectionLength - 9 /* Size of the rest of the fields before descriptors */ - programInfoLength - 4 /* CRC size */; while (entriesSize > 0) { - int streamType = tsBuffer.readBits(8); - tsBuffer.skipBits(3); - int elementaryPid = tsBuffer.readBits(13); - tsBuffer.skipBits(4); + tsBuffer.readBytes(scratch, 5); + int streamType = scratch.readBits(8); + scratch.skipBits(3); // reserved + int elementaryPid = scratch.readBits(13); + scratch.skipBits(4); // reserved + int esInfoLength = scratch.readBits(12); - int esInfoLength = tsBuffer.readBits(12); // Skip the descriptors. - tsBuffer.skipBytes(esInfoLength); + tsBuffer.skip(esInfoLength); entriesSize -= esInfoLength + 5; if (sampleQueues.get(streamType) != null) { @@ -436,7 +452,7 @@ public final class TsExtractor { private class PesReader extends TsPayloadReader { // Reusable buffer for incomplete PES data. - private final BitArray pesBuffer; + private final ParsableByteArray pesBuffer; // Parses PES payload and extracts individual samples. private final PesPayloadReader pesPayloadReader; @@ -445,11 +461,11 @@ public final class TsExtractor { public PesReader(PesPayloadReader pesPayloadReader) { this.pesPayloadReader = pesPayloadReader; this.packetLength = -1; - pesBuffer = new BitArray(); + pesBuffer = new ParsableByteArray(); } @Override - public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { + public void read(ParsableByteArray tsBuffer, boolean payloadUnitStartIndicator) { if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) { if (packetLength == 0) { // The length of the previous packet was unspecified. We've now seen the start of the @@ -477,49 +493,50 @@ public final class TsExtractor { } private void readPacketStart() { - int startCodePrefix = pesBuffer.readBits(24); + pesBuffer.readBytes(scratch, 3); + int startCodePrefix = scratch.readBits(24); if (startCodePrefix != 0x000001) { Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); pesBuffer.reset(); packetLength = -1; } else { - // TODO: Read and use stream_id. - pesBuffer.skipBits(8); // Skip stream_id. - packetLength = pesBuffer.readBits(16); + pesBuffer.skip(1); // stream_id. + packetLength = pesBuffer.readUnsignedShort(); } } private void readPacketBody() { - // Skip some fields/flags. - // TODO: might need to use data_alignment_indicator. - pesBuffer.skipBits(8); // 2+2+1+1+1+1 - boolean ptsFlag = pesBuffer.readBit(); - // Skip DTS flag. - pesBuffer.skipBits(1); - // Skip some fields/flags. - pesBuffer.skipBits(6); // 1+1+1+1+1+1 + // '10' (2), PES_scrambling_control (2), PES_priority (1), data_alignment_indicator (1), + // copyright (1), original_or_copy (1) + pesBuffer.skip(1); - int headerDataLength = pesBuffer.readBits(8); + pesBuffer.readBytes(scratch, 1); + boolean ptsFlag = scratch.readBit(); + // Last 7 bits of scratch are skipped: DTS_flag (1), ESCR_flag (1), ES_rate_flag (1), + // DSM_trick_mode_flag (1), additional_copy_info_flag (1), PES_CRC_flag (1), + // PES_extension_flag (1) + + int headerDataLength = pesBuffer.readUnsignedByte(); if (headerDataLength == 0) { headerDataLength = pesBuffer.bytesLeft(); } long timeUs = 0; if (ptsFlag) { - // Skip prefix. - pesBuffer.skipBits(4); - long pts = pesBuffer.readBitsLong(3) << 30; - pesBuffer.skipBits(1); - pts |= pesBuffer.readBitsLong(15) << 15; - pesBuffer.skipBits(1); - pts |= pesBuffer.readBitsLong(15); - pesBuffer.skipBits(1); + pesBuffer.readBytes(scratch, 5); + scratch.skipBits(4); // '0010' + long pts = scratch.readBitsLong(3) << 30; + scratch.skipBits(1); // marker_bit + pts |= scratch.readBitsLong(15) << 15; + scratch.skipBits(1); // marker_bit + pts |= scratch.readBitsLong(15); + scratch.skipBits(1); // marker_bit timeUs = ptsToTimeUs(pts); // Skip the rest of the header. - pesBuffer.skipBytes(headerDataLength - 5); + pesBuffer.skip(headerDataLength - 5); } else { // Skip the rest of the header. - pesBuffer.skipBytes(headerDataLength); + pesBuffer.skip(headerDataLength); } int payloadSize; @@ -709,7 +726,7 @@ public final class TsExtractor { * @param sampleTimeUs The sample time stamp. * @param isKeyframe True if the sample is a keyframe. False otherwise. */ - protected void addSample(int type, BitArray buffer, int sampleSize, long sampleTimeUs, + protected void addSample(int type, ParsableByteArray buffer, int sampleSize, long sampleTimeUs, boolean isKeyframe) { Sample sample = getSample(type); addToSample(sample, buffer, sampleSize); @@ -723,7 +740,7 @@ public final class TsExtractor { internalQueueSample(sample); } - protected void addToSample(Sample sample, BitArray buffer, int size) { + protected void addToSample(Sample sample, ParsableByteArray buffer, int size) { if (sample.data.length - sample.size < size) { sample.expand(size - sample.data.length + sample.size); } @@ -764,7 +781,7 @@ public final class TsExtractor { internalQueue.add(sample); } - public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs); + public abstract void read(ParsableByteArray pesBuffer, int pesPayloadSize, long pesTimeUs); } @@ -798,7 +815,7 @@ public final class TsExtractor { } @Override - public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { + public void read(ParsableByteArray pesBuffer, int pesPayloadSize, long pesTimeUs) { // Read leftover frame data from previous PES packet. pesPayloadSize -= readOneH264Frame(pesBuffer, true); @@ -824,10 +841,10 @@ public final class TsExtractor { } @SuppressLint("InlinedApi") - private int readOneH264Frame(BitArray pesBuffer, boolean remainderOnly) { - byte[] pesData = pesBuffer.getData(); - int pesOffset = pesBuffer.getByteOffset(); - int pesLimit = pesBuffer.limit(); + private int readOneH264Frame(ParsableByteArray pesBuffer, boolean remainderOnly) { + byte[] pesData = pesBuffer.data; + int pesOffset = pesBuffer.getPosition(); + int pesLimit = pesBuffer.length(); int searchOffset = pesOffset + (remainderOnly ? 0 : 3); int audOffset = Mp4Util.findNalUnit(pesData, searchOffset, pesLimit, NAL_UNIT_TYPE_AUD); @@ -839,7 +856,7 @@ public final class TsExtractor { } addToSample(currentSample, pesBuffer, bytesToNextAud); } else { - pesBuffer.skipBytes(bytesToNextAud); + pesBuffer.skip(bytesToNextAud); } return bytesToNextAud; } @@ -854,8 +871,8 @@ public final class TsExtractor { return; } // Determine the length of the units, and copy them to build the initialization data. - int spsLength = Mp4Util.findNextNalUnit(sampleData, spsOffset + 3, sampleSize) - spsOffset; - int ppsLength = Mp4Util.findNextNalUnit(sampleData, ppsOffset + 3, sampleSize) - ppsOffset; + int spsLength = Mp4Util.findNalUnit(sampleData, spsOffset + 3, sampleSize) - spsOffset; + int ppsLength = Mp4Util.findNalUnit(sampleData, ppsOffset + 3, sampleSize) - ppsOffset; byte[] spsData = new byte[spsLength]; byte[] ppsData = new byte[ppsLength]; System.arraycopy(sampleData, spsOffset, spsData, 0, spsLength); @@ -866,15 +883,11 @@ public final class TsExtractor { // Unescape and then parse the SPS unit. byte[] unescapedSps = unescapeStream(spsData, 0, spsLength); - BitArray bitArray = new BitArray(unescapedSps, unescapedSps.length); - - // Skip the NAL header. - bitArray.skipBytes(4); + ParsableBitArray bitArray = new ParsableBitArray(unescapedSps); + bitArray.skipBits(32); // NAL header int profileIdc = bitArray.readBits(8); - // Skip 6 constraint bits, 2 reserved bits and level_idc. - bitArray.skipBytes(2); - // Skip seq_parameter_set_id. - bitArray.readUnsignedExpGolombCodedInt(); + bitArray.skipBits(16); // constraint bits (6), reserved (2) and level_idc (8) + bitArray.readUnsignedExpGolombCodedInt(); // seq_parameter_set_id int chromaFormatIdc = 1; // Default is 4:2:0 if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244 @@ -882,15 +895,11 @@ public final class TsExtractor { || profileIdc == 128 || profileIdc == 138) { chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); if (chromaFormatIdc == 3) { - // Skip separate_colour_plane_flag - bitArray.skipBits(1); + bitArray.skipBits(1); // separate_colour_plane_flag } - // Skip bit_depth_luma_minus8 - bitArray.readUnsignedExpGolombCodedInt(); - // Skip bit_depth_chroma_minus8 - bitArray.readUnsignedExpGolombCodedInt(); - // Skip qpprime_y_zero_transform_bypass_flag - bitArray.skipBits(1); + bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8 + bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8 + bitArray.skipBits(1); // qpprime_y_zero_transform_bypass_flag boolean seqScalingMatrixPresentFlag = bitArray.readBit(); if (seqScalingMatrixPresentFlag) { int limit = (chromaFormatIdc != 3) ? 8 : 12; @@ -902,39 +911,32 @@ public final class TsExtractor { } } } - // Skip log2_max_frame_num_minus4 - bitArray.readUnsignedExpGolombCodedInt(); + + bitArray.readUnsignedExpGolombCodedInt(); // log2_max_frame_num_minus4 long picOrderCntType = bitArray.readUnsignedExpGolombCodedInt(); if (picOrderCntType == 0) { - // Skip log2_max_pic_order_cnt_lsb_minus4 - bitArray.readUnsignedExpGolombCodedInt(); + bitArray.readUnsignedExpGolombCodedInt(); // log2_max_pic_order_cnt_lsb_minus4 } else if (picOrderCntType == 1) { - // Skip delta_pic_order_always_zero_flag - bitArray.skipBits(1); - // Skip offset_for_non_ref_pic - bitArray.readSignedExpGolombCodedInt(); - // Skip offset_for_top_to_bottom_field - bitArray.readSignedExpGolombCodedInt(); + bitArray.skipBits(1); // delta_pic_order_always_zero_flag + bitArray.readSignedExpGolombCodedInt(); // offset_for_non_ref_pic + bitArray.readSignedExpGolombCodedInt(); // offset_for_top_to_bottom_field long numRefFramesInPicOrderCntCycle = bitArray.readUnsignedExpGolombCodedInt(); for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) { - // Skip offset_for_ref_frame[i] - bitArray.readUnsignedExpGolombCodedInt(); + bitArray.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i] } } - // Skip max_num_ref_frames - bitArray.readUnsignedExpGolombCodedInt(); - // Skip gaps_in_frame_num_value_allowed_flag - bitArray.skipBits(1); + bitArray.readUnsignedExpGolombCodedInt(); // max_num_ref_frames + bitArray.skipBits(1); // gaps_in_frame_num_value_allowed_flag + int picWidthInMbs = bitArray.readUnsignedExpGolombCodedInt() + 1; int picHeightInMapUnits = bitArray.readUnsignedExpGolombCodedInt() + 1; boolean frameMbsOnlyFlag = bitArray.readBit(); int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits; if (!frameMbsOnlyFlag) { - // Skip mb_adaptive_frame_field_flag - bitArray.skipBits(1); + bitArray.skipBits(1); // mb_adaptive_frame_field_flag } - // Skip direct_8x8_inference_flag - bitArray.skipBits(1); + + bitArray.skipBits(1); // direct_8x8_inference_flag int frameWidth = picWidthInMbs * 16; int frameHeight = frameHeightInMbs * 16; boolean frameCroppingFlag = bitArray.readBit(); @@ -962,7 +964,7 @@ public final class TsExtractor { frameWidth, frameHeight, initializationData)); } - private void skipScalingList(BitArray bitArray, int size) { + private void skipScalingList(ParsableBitArray bitArray, int size) { int lastScale = 8; int nextScale = 8; for (int i = 0; i < size; i++) { @@ -1029,13 +1031,13 @@ public final class TsExtractor { // SEI data, used for Closed Captions. private static final int NAL_UNIT_TYPE_SEI = 6; - private final BitArray seiBuffer; + private final ParsableByteArray seiBuffer; private final TreeSet internalQueue; public SeiReader(SamplePool samplePool) { super(samplePool); setMediaFormat(MediaFormat.createEia608Format()); - seiBuffer = new BitArray(); + seiBuffer = new ParsableByteArray(); internalQueue = new TreeSet(this); } @@ -1043,12 +1045,12 @@ public final class TsExtractor { public void read(byte[] data, int length, long pesTimeUs) { seiBuffer.reset(data, length); while (seiBuffer.bytesLeft() > 0) { - int currentOffset = seiBuffer.getByteOffset(); + int currentOffset = seiBuffer.getPosition(); int seiOffset = Mp4Util.findNalUnit(data, currentOffset, length, NAL_UNIT_TYPE_SEI); if (seiOffset == length) { return; } - seiBuffer.skipBytes(seiOffset + 4 - currentOffset); + seiBuffer.skip(seiOffset + 4 - currentOffset); int ccDataSize = Eia608Parser.parseHeader(seiBuffer); if (ccDataSize > 0) { addSample(Sample.TYPE_MISC, seiBuffer, ccDataSize, pesTimeUs, true); @@ -1084,17 +1086,17 @@ public final class TsExtractor { */ private class AdtsReader extends PesPayloadReader { - private final BitArray adtsBuffer; + private final ParsableByteArray adtsBuffer; private long timeUs; private long frameDurationUs; public AdtsReader(SamplePool samplePool) { super(samplePool); - adtsBuffer = new BitArray(); + adtsBuffer = new ParsableByteArray(); } @Override - public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { + public void read(ParsableByteArray pesBuffer, int pesPayloadSize, long pesTimeUs) { boolean needToProcessLeftOvers = !adtsBuffer.isEmpty(); adtsBuffer.append(pesBuffer, pesPayloadSize); // If there are leftovers from previous PES packet, process it with last calculated timeUs. @@ -1114,24 +1116,26 @@ public final class TsExtractor { } int offsetToSyncWord = findOffsetToSyncWord(); - adtsBuffer.skipBytes(offsetToSyncWord); + adtsBuffer.skip(offsetToSyncWord); - int adtsStartOffset = adtsBuffer.getByteOffset(); + int adtsStartOffset = adtsBuffer.getPosition(); if (adtsBuffer.bytesLeft() < 7) { - adtsBuffer.setByteOffset(adtsStartOffset); + adtsBuffer.setPosition(adtsStartOffset); adtsBuffer.clearReadData(); return false; } - adtsBuffer.skipBits(15); - boolean hasCRC = !adtsBuffer.readBit(); + adtsBuffer.readBytes(scratch, 2); + scratch.skipBits(15); + boolean hasCRC = !scratch.readBit(); + adtsBuffer.readBytes(scratch, 5); if (!hasMediaFormat()) { - int audioObjectType = adtsBuffer.readBits(2) + 1; - int sampleRateIndex = adtsBuffer.readBits(4); - adtsBuffer.skipBits(1); - int channelConfig = adtsBuffer.readBits(3); + int audioObjectType = scratch.readBits(2) + 1; + int sampleRateIndex = scratch.readBits(4); + scratch.skipBits(1); + int channelConfig = scratch.readBits(3); byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAudioSpecificConfig( audioObjectType, sampleRateIndex, channelConfig); @@ -1144,24 +1148,23 @@ public final class TsExtractor { frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate; setMediaFormat(mediaFormat); } else { - adtsBuffer.skipBits(10); + scratch.skipBits(10); } - - adtsBuffer.skipBits(4); - int frameSize = adtsBuffer.readBits(13); - adtsBuffer.skipBits(13); + scratch.skipBits(4); + int frameSize = scratch.readBits(13); + scratch.skipBits(13); // Decrement frame size by ADTS header size and CRC. if (hasCRC) { // Skip CRC. - adtsBuffer.skipBytes(2); + adtsBuffer.skip(2); frameSize -= 9; } else { frameSize -= 7; } if (frameSize > adtsBuffer.bytesLeft()) { - adtsBuffer.setByteOffset(adtsStartOffset); + adtsBuffer.setPosition(adtsStartOffset); adtsBuffer.clearReadData(); return false; } @@ -1183,9 +1186,9 @@ public final class TsExtractor { * position of the end of the data is returned. */ private int findOffsetToSyncWord() { - byte[] adtsData = adtsBuffer.getData(); - int startOffset = adtsBuffer.getByteOffset(); - int endOffset = startOffset + adtsBuffer.bytesLeft(); + byte[] adtsData = adtsBuffer.data; + int startOffset = adtsBuffer.getPosition(); + int endOffset = adtsBuffer.length(); for (int i = startOffset; i < endOffset - 1; i++) { int syncBits = ((adtsData[i] & 0xFF) << 8) | (adtsData[i + 1] & 0xFF); if ((syncBits & 0xFFF0) == 0xFFF0 && syncBits != 0xFFFF) { @@ -1209,7 +1212,7 @@ public final class TsExtractor { @SuppressLint("InlinedApi") @Override - public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { + public void read(ParsableByteArray pesBuffer, int pesPayloadSize, long pesTimeUs) { addSample(Sample.TYPE_MISC, pesBuffer, pesPayloadSize, pesTimeUs, true); } diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index 0b28614465..1ec0e363dd 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -38,7 +38,7 @@ public class Id3Parser implements MetadataParser> { public Map parse(byte[] data, int size) throws UnsupportedEncodingException, ParserException { Map metadata = new HashMap(); - ParsableByteArray id3Data = new ParsableByteArray(data); + ParsableByteArray id3Data = new ParsableByteArray(data, size); int id3Size = parseId3Header(id3Data); while (id3Size > 0) { diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java index f5a8726a85..6a7123d555 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java @@ -106,7 +106,7 @@ public final class Mp4Util { * @param endOffset The offset (exclusive) in the data to end the search. * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. */ - public static int findNextNalUnit(byte[] data, int startOffset, int endOffset) { + public static int findNalUnit(byte[] data, int startOffset, int endOffset) { return findNalUnit(data, startOffset, endOffset, -1); } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java index 8d4b0f5b06..bce5c5de35 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java @@ -15,8 +15,9 @@ */ package com.google.android.exoplayer.text.eia608; -import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; import java.util.List; @@ -80,11 +81,11 @@ public class Eia608Parser { 0xFB // 3F: 251 'รป' "Latin small letter U with circumflex" }; - private final BitArray seiBuffer; + private final ParsableBitArray seiBuffer; private final StringBuilder stringBuilder; /* package */ Eia608Parser() { - seiBuffer = new BitArray(); + seiBuffer = new ParsableBitArray(); stringBuilder = new StringBuilder(); } @@ -98,10 +99,10 @@ public class Eia608Parser { } stringBuilder.setLength(0); - seiBuffer.reset(data, size); + seiBuffer.reset(data); seiBuffer.skipBits(3); // reserved + process_cc_data_flag + zero_bit int ccCount = seiBuffer.readBits(5); - seiBuffer.skipBytes(1); + seiBuffer.skipBits(8); for (int i = 0; i < ccCount; i++) { seiBuffer.skipBits(5); // one_bit + reserved @@ -170,7 +171,7 @@ public class Eia608Parser { * @param seiBuffer The buffer to read from. * @return The size of closed captions data. */ - public static int parseHeader(BitArray seiBuffer) { + public static int parseHeader(ParsableByteArray seiBuffer) { int b = 0; int payloadType = 0; @@ -197,11 +198,11 @@ public class Eia608Parser { if (countryCode != COUNTRY_CODE) { return 0; } - int providerCode = seiBuffer.readBits(16); + int providerCode = seiBuffer.readUnsignedShort(); if (providerCode != PROVIDER_CODE) { return 0; } - int userIdentifier = seiBuffer.readBits(32); + int userIdentifier = seiBuffer.readInt(); if (userIdentifier != USER_ID) { return 0; } diff --git a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java b/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java similarity index 51% rename from library/src/main/java/com/google/android/exoplayer/util/BitArray.java rename to library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java index 4a57a0b2f9..a1f224a1f0 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java @@ -15,51 +15,37 @@ */ package com.google.android.exoplayer.util; -import com.google.android.exoplayer.upstream.DataSource; - -import java.io.IOException; - /** * Wraps a byte array, providing methods that allow it to be read as a bitstream. */ -public final class BitArray { +public final class ParsableBitArray { private byte[] data; - // The length of the valid data. - private int limit; - // The offset within the data, stored as the current byte offset, and the bit offset within that // byte (from 0 to 7). private int byteOffset; private int bitOffset; - public BitArray() { - } - - public BitArray(byte[] data, int limit) { - this.data = data; - this.limit = limit; - } + /** Creates a new instance that initially has no backing data. */ + public ParsableBitArray() {} /** - * Clears all data, setting the offset and limit to zero. - */ - public void reset() { - byteOffset = 0; - bitOffset = 0; - limit = 0; - } - - /** - * Resets to wrap the specified data, setting the offset to zero. + * Creates a new instance that wraps an existing array. * * @param data The data to wrap. - * @param limit The limit to set. */ - public void reset(byte[] data, int limit) { + public ParsableBitArray(byte[] data) { + this.data = data; + } + + /** + * Updates the instance to wrap {@code data}, and resets the position to zero. + * + * @param data The array to wrap. + */ + public void reset(byte[] data) { this.data = data; - this.limit = limit; byteOffset = 0; bitOffset = 0; } @@ -74,89 +60,36 @@ public final class BitArray { } /** - * Gets the current byte offset. + * Gets the current bit offset. * - * @return The current byte offset. + * @return The current bit offset. */ - public int getByteOffset() { - return byteOffset; + public int getPosition() { + return byteOffset * 8 + bitOffset; } /** - * Sets the current byte offset. + * Sets the current bit offset. * - * @param byteOffset The byte offset to set. + * @param position The position to set. */ - public void setByteOffset(int byteOffset) { - this.byteOffset = byteOffset; + public void setPosition(int position) { + byteOffset = position / 8; + bitOffset = position - (byteOffset * 8); } /** - * Appends data from a {@link DataSource}. + * Skips bits and moves current reading position forward. * - * @param dataSource The {@link DataSource} from which to read. - * @param length The maximum number of bytes to read and append. - * @return The number of bytes that were read and appended, or -1 if no more data is available. - * @throws IOException If an error occurs reading from the source. + * @param n The number of bits to skip. */ - public int append(DataSource dataSource, int length) throws IOException { - expand(length); - int bytesRead = dataSource.read(data, limit, length); - if (bytesRead == -1) { - return -1; + public void skipBits(int n) { + byteOffset += (n / 8); + bitOffset += (n % 8); + if (bitOffset > 7) { + byteOffset++; + bitOffset -= 8; } - limit += bytesRead; - return bytesRead; - } - - /** - * Appends data from another {@link BitArray}. - * - * @param bitsArray The {@link BitArray} whose data should be appended. - * @param length The number of bytes to read and append. - */ - public void append(BitArray bitsArray, int length) { - expand(length); - bitsArray.readBytes(data, limit, length); - limit += length; - } - - private void expand(int length) { - if (data == null) { - data = new byte[length]; - return; - } - if (data.length - limit < length) { - byte[] newBuffer = new byte[limit + length]; - System.arraycopy(data, 0, newBuffer, 0, limit); - data = newBuffer; - } - } - - /** - * Clears data that has already been read, moving the remaining data to the start of the buffer. - */ - public void clearReadData() { - System.arraycopy(data, byteOffset, data, 0, limit - byteOffset); - limit -= byteOffset; - byteOffset = 0; - } - - /** - * Reads a single unsigned byte. - * - * @return The value of the parsed byte. - */ - public int readUnsignedByte() { - int value; - if (bitOffset != 0) { - value = ((data[byteOffset] & 0xFF) << bitOffset) - | ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset)); - } else { - value = data[byteOffset]; - } - byteOffset++; - return value & 0xFF; } /** @@ -220,71 +153,6 @@ public final class BitArray { return retval; } - private int getUnsignedByte(int offset) { - return data[offset] & 0xFF; - } - - /** - * Skips bits and moves current reading position forward. - * - * @param n The number of bits to skip. - */ - public void skipBits(int n) { - byteOffset += (n / 8); - bitOffset += (n % 8); - if (bitOffset > 7) { - byteOffset++; - bitOffset -= 8; - } - } - - /** - * Skips bytes and moves current reading position forward. - * - * @param n The number of bytes to skip. - */ - public void skipBytes(int n) { - byteOffset += n; - } - - /** - * Reads multiple bytes and copies them into provided byte array. - *

- * The read position must be at a whole byte boundary for this method to be called. - * - * @param out The byte array to copy read data. - * @param offset The offset in the out byte array. - * @param length The length of the data to read - * @throws IllegalStateException If the method is called with the read position not at a whole - * byte boundary. - */ - public void readBytes(byte[] out, int offset, int length) { - Assertions.checkState(bitOffset == 0); - System.arraycopy(data, byteOffset, out, offset, length); - byteOffset += length; - } - - /** - * @return The number of whole bytes that are available to read. - */ - public int bytesLeft() { - return limit - byteOffset; - } - - /** - * @return The limit of the data, specified in whole bytes. - */ - public int limit() { - return limit; - } - - /** - * @return Whether or not there is any data available. - */ - public boolean isEmpty() { - return limit == 0; - } - /** * Reads an unsigned Exp-Golomb-coded format integer. * @@ -304,6 +172,22 @@ public final class BitArray { return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2); } + private int readUnsignedByte() { + int value; + if (bitOffset != 0) { + value = ((data[byteOffset] & 0xFF) << bitOffset) + | ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset)); + } else { + value = data[byteOffset]; + } + byteOffset++; + return value & 0xFF; + } + + private int getUnsignedByte(int offset) { + return data[offset] & 0xFF; + } + private int readExpGolombCodeNum() { int leadingZeros = 0; while (!readBit()) { diff --git a/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java index c516c0c926..98b4e2aa07 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer.util; import java.nio.ByteBuffer; +import java.util.Arrays; /** * Wraps a byte array, providing a set of methods for parsing data from it. Numerical values are @@ -23,27 +24,82 @@ import java.nio.ByteBuffer; */ public final class ParsableByteArray { - public final byte[] data; + public byte[] data; private int position; + private int limit; + + /** Creates a new instance that initially has no backing data. */ + public ParsableByteArray() {} /** Creates a new instance with {@code length} bytes. */ public ParsableByteArray(int length) { this.data = new byte[length]; + limit = data.length; } /** * Creates a new instance that wraps an existing array. * * @param data The data to wrap. + * @param limit The limit. */ - public ParsableByteArray(byte[] data) { + public ParsableByteArray(byte[] data, int limit) { this.data = data; + this.limit = limit; + } + + /** + * Updates the instance to wrap {@code data}, and resets the position to zero. + * + * @param data The array to wrap. + * @param limit The limit. + */ + public void reset(byte[] data, int limit) { + this.data = data; + this.limit = limit; + position = 0; + } + + /** + * Sets the position and limit to zero. + */ + public void reset() { + position = 0; + limit = 0; + } + + // TODO: This method is temporary + public void append(ParsableByteArray buffer, int length) { + if (data == null) { + data = new byte[length]; + } else if (data.length < limit + length) { + data = Arrays.copyOf(data, limit + length); + } + buffer.readBytes(data, limit, length); + limit += length; + } + + // TODO: This method is temporary + public void clearReadData() { + System.arraycopy(data, position, data, 0, limit - position); + limit -= position; + position = 0; + } + + // TODO: This method is temporary + public boolean isEmpty() { + return limit == 0; + } + + /** Returns the number of bytes yet to be read. */ + public int bytesLeft() { + return limit - position; } /** Returns the number of bytes in the array. */ public int length() { - return data.length; + return limit; } /** Returns the current offset in the array, in bytes. */ @@ -60,7 +116,7 @@ public final class ParsableByteArray { */ public void setPosition(int position) { // It is fine for position to be at the end of the array. - Assertions.checkArgument(position >= 0 && position <= data.length); + Assertions.checkArgument(position >= 0 && position <= limit); this.position = position; } @@ -70,10 +126,26 @@ public final class ParsableByteArray { * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the * array. */ + // TODO: Rename to skipBytes so that it's clearer how much data is being skipped in code where + // both ParsableBitArray and ParsableByteArray are in use. public void skip(int bytes) { setPosition(position + bytes); } + /** + * Reads the next {@code length} bytes into {@code bitArray}, and resets the position of + * {@code bitArray} to zero. + * + * @param bitArray The {@link ParsableBitArray} into which the bytes should be read. + * @param length The number of bytes to write. + */ + // TODO: It's possible to have bitArray directly index into the same array as is being wrapped + // by this instance. Decide whether it's worth doing this. + public void readBytes(ParsableBitArray bitArray, int length) { + readBytes(bitArray.getData(), 0, length); + bitArray.setPosition(0); + } + /** * Reads the next {@code length} bytes into {@code buffer} at {@code offset}. * From 92f085bc5823243ca0b8f136283256fc5a188737 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 11 Feb 2015 14:57:07 +0000 Subject: [PATCH 03/17] Remove intermediate copy steps in TsExtractor. 1. AdtsReader would previously copy all data through an intermediate adtsBuffer. This change eliminates the additional copy step, and instead copies directly into Sample objects. 2. PesReader would previously accumulate a whole packet by copying multiple TS packets into an intermediate buffer. This change eliminates this copy step. After the change, TS packet buffers are propagated directly to PesPayloadReaders, which are required to handle partial payload data correctly. The copy steps in the extractor are simplified from: DataSource->Ts_BitArray->Pes_BitArray->Sample->SampleHolder To: DataSource->Ts_BitArray->Sample->SampleHolder Issue: #278 --- .../android/exoplayer/hls/TsExtractor.java | 657 +++++++++++------- .../exoplayer/util/ParsableByteArray.java | 35 +- 2 files changed, 425 insertions(+), 267 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index 9e43da5324..ae9ec2159f 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -65,7 +65,7 @@ public final class TsExtractor { private final SamplePool samplePool; private final boolean shouldSpliceIn; private final long firstSampleTimestamp; - /* package */ final ParsableBitArray scratch; + private final ParsableBitArray tsScratch; // Accessed only by the consuming thread. private boolean spliceConfigured; @@ -83,7 +83,7 @@ public final class TsExtractor { this.firstSampleTimestamp = firstSampleTimestamp; this.samplePool = samplePool; this.shouldSpliceIn = shouldSpliceIn; - scratch = new ParsableBitArray(new byte[5]); + tsScratch = new ParsableBitArray(new byte[3]); tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); sampleQueues = new SparseArray(); tsPayloadReaders = new SparseArray(); @@ -254,20 +254,21 @@ public final class TsExtractor { // Reset before reading the packet. tsPacketBytesRead = 0; tsPacketBuffer.setPosition(0); + tsPacketBuffer.setLimit(TS_PACKET_SIZE); int syncByte = tsPacketBuffer.readUnsignedByte(); if (syncByte != TS_SYNC_BYTE) { return bytesRead; } - tsPacketBuffer.readBytes(scratch, 3); - scratch.skipBits(1); // transport_error_indicator - boolean payloadUnitStartIndicator = scratch.readBit(); - scratch.skipBits(1); // transport_priority - int pid = scratch.readBits(13); - scratch.skipBits(2); // transport_scrambling_control - boolean adaptationFieldExists = scratch.readBit(); - boolean payloadExists = scratch.readBit(); + tsPacketBuffer.readBytes(tsScratch, 3); + tsScratch.skipBits(1); // transport_error_indicator + boolean payloadUnitStartIndicator = tsScratch.readBit(); + tsScratch.skipBits(1); // transport_priority + int pid = tsScratch.readBits(13); + tsScratch.skipBits(2); // transport_scrambling_control + boolean adaptationFieldExists = tsScratch.readBit(); + boolean payloadExists = tsScratch.readBit(); // Last 4 bits of scratch are skipped: continuity_counter // Skip the adaptation field. @@ -280,7 +281,7 @@ public final class TsExtractor { if (payloadExists) { TsPayloadReader payloadReader = tsPayloadReaders.get(pid); if (payloadReader != null) { - payloadReader.read(tsPacketBuffer, payloadUnitStartIndicator); + payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); } } @@ -332,11 +333,11 @@ public final class TsExtractor { } /** - * Parses payload data. + * Parses TS packet payload data. */ private abstract static class TsPayloadReader { - public abstract void read(ParsableByteArray tsBuffer, boolean payloadUnitStartIndicator); + public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator); } @@ -345,26 +346,32 @@ public final class TsExtractor { */ private class PatReader extends TsPayloadReader { + private final ParsableBitArray patScratch; + + public PatReader() { + patScratch = new ParsableBitArray(new byte[4]); + } + @Override - public void read(ParsableByteArray tsBuffer, boolean payloadUnitStartIndicator) { + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { // Skip pointer. if (payloadUnitStartIndicator) { - int pointerField = tsBuffer.readUnsignedByte(); - tsBuffer.skip(pointerField); + int pointerField = data.readUnsignedByte(); + data.skip(pointerField); } - tsBuffer.readBytes(scratch, 3); - scratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) - int sectionLength = scratch.readBits(12); + data.readBytes(patScratch, 3); + patScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) + int sectionLength = patScratch.readBits(12); // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1), // section_number (8), last_section_number (8) - tsBuffer.skip(5); + data.skip(5); int programCount = (sectionLength - 9) / 4; for (int i = 0; i < programCount; i++) { - tsBuffer.readBytes(scratch, 4); - scratch.skipBits(19); // program_number (16), reserved (3) - int pid = scratch.readBits(13); + data.readBytes(patScratch, 4); + patScratch.skipBits(19); // program_number (16), reserved (3) + int pid = patScratch.readBits(13); tsPayloadReaders.put(pid, new PmtReader()); } @@ -378,42 +385,48 @@ public final class TsExtractor { */ private class PmtReader extends TsPayloadReader { + private final ParsableBitArray pmtScratch; + + public PmtReader() { + pmtScratch = new ParsableBitArray(new byte[5]); + } + @Override - public void read(ParsableByteArray tsBuffer, boolean payloadUnitStartIndicator) { + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { // Skip pointer. if (payloadUnitStartIndicator) { - int pointerField = tsBuffer.readUnsignedByte(); - tsBuffer.skip(pointerField); + int pointerField = data.readUnsignedByte(); + data.skip(pointerField); } - tsBuffer.readBytes(scratch, 3); - scratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) - int sectionLength = scratch.readBits(12); + data.readBytes(pmtScratch, 3); + pmtScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) + int sectionLength = pmtScratch.readBits(12); // program_number (16), reserved (2), version_number (5), current_next_indicator (1), // section_number (8), last_section_number (8), reserved (3), PCR_PID (13) // Skip the rest of the PMT header. - tsBuffer.skip(7); + data.skip(7); - tsBuffer.readBytes(scratch, 2); - scratch.skipBits(4); - int programInfoLength = scratch.readBits(12); + data.readBytes(pmtScratch, 2); + pmtScratch.skipBits(4); + int programInfoLength = pmtScratch.readBits(12); // Skip the descriptors. - tsBuffer.skip(programInfoLength); + data.skip(programInfoLength); int entriesSize = sectionLength - 9 /* Size of the rest of the fields before descriptors */ - programInfoLength - 4 /* CRC size */; while (entriesSize > 0) { - tsBuffer.readBytes(scratch, 5); - int streamType = scratch.readBits(8); - scratch.skipBits(3); // reserved - int elementaryPid = scratch.readBits(13); - scratch.skipBits(4); // reserved - int esInfoLength = scratch.readBits(12); + data.readBytes(pmtScratch, 5); + int streamType = pmtScratch.readBits(8); + pmtScratch.skipBits(3); // reserved + int elementaryPid = pmtScratch.readBits(13); + pmtScratch.skipBits(4); // reserved + int esInfoLength = pmtScratch.readBits(12); // Skip the descriptors. - tsBuffer.skip(esInfoLength); + data.skip(esInfoLength); entriesSize -= esInfoLength + 5; if (sampleQueues.get(streamType) != null) { @@ -451,105 +464,172 @@ public final class TsExtractor { */ private class PesReader extends TsPayloadReader { - // Reusable buffer for incomplete PES data. - private final ParsableByteArray pesBuffer; - // Parses PES payload and extracts individual samples. + private static final int STATE_FINDING_HEADER = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_HEADER_EXTENSION = 2; + private static final int STATE_READING_BODY = 3; + + private static final int HEADER_SIZE = 9; + private static final int MAX_HEADER_EXTENSION_SIZE = 5; + + private final ParsableBitArray pesScratch; private final PesPayloadReader pesPayloadReader; - private int packetLength; + private int state; + private int bytesRead; + private boolean bodyStarted; + + private boolean ptsFlag; + private int extendedHeaderLength; + + private int payloadSize; + + private long timeUs; public PesReader(PesPayloadReader pesPayloadReader) { this.pesPayloadReader = pesPayloadReader; - this.packetLength = -1; - pesBuffer = new ParsableByteArray(); + pesScratch = new ParsableBitArray(new byte[HEADER_SIZE]); + state = STATE_FINDING_HEADER; } @Override - public void read(ParsableByteArray tsBuffer, boolean payloadUnitStartIndicator) { - if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) { - if (packetLength == 0) { - // The length of the previous packet was unspecified. We've now seen the start of the - // next one, so consume the previous packet's body. - readPacketBody(); - } else { - // Either we didn't have enough data to read the length of the previous packet, or we - // did read the length but didn't receive that amount of data. Neither case is expected. - Log.w(TAG, "Unexpected packet fragment of length " + pesBuffer.bytesLeft()); - pesBuffer.reset(); - packetLength = -1; + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + if (payloadUnitStartIndicator) { + switch (state) { + case STATE_FINDING_HEADER: + case STATE_READING_HEADER: + // Expected. + break; + case STATE_READING_HEADER_EXTENSION: + Log.w(TAG, "Unexpected start indicator reading extended header"); + break; + case STATE_READING_BODY: + // If payloadSize == -1 then the length of the previous packet was unspecified, and so + // we only know that it's finished now that we've seen the start of the next one. This + // is expected. If payloadSize != -1, then the length of the previous packet was known, + // but we didn't receive that amount of data. This is not expected. + if (payloadSize != -1) { + Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes"); + } + // Either way, if the body was started, notify the reader that it has now finished. + if (bodyStarted) { + pesPayloadReader.packetFinished(); + } + break; + } + setState(STATE_READING_HEADER); + } + + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_HEADER: + data.skip(data.bytesLeft()); + break; + case STATE_READING_HEADER: + if (continueRead(data, pesScratch.getData(), HEADER_SIZE)) { + setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER); + } + break; + case STATE_READING_HEADER_EXTENSION: + int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); + // Read as much of the extended header as we're interested in, and skip the rest. + if (continueRead(data, pesScratch.getData(), readLength) + && continueRead(data, null, extendedHeaderLength)) { + parseHeaderExtension(); + bodyStarted = false; + setState(STATE_READING_BODY); + } + break; + case STATE_READING_BODY: + readLength = data.bytesLeft(); + int padding = payloadSize == -1 ? 0 : readLength - payloadSize; + if (padding > 0) { + readLength -= padding; + data.setLimit(data.getPosition() + readLength); + } + pesPayloadReader.consume(data, timeUs, !bodyStarted); + bodyStarted = true; + if (payloadSize != -1) { + payloadSize -= readLength; + if (payloadSize == 0) { + pesPayloadReader.packetFinished(); + setState(STATE_READING_HEADER); + } + } + break; } } - - pesBuffer.append(tsBuffer, tsBuffer.bytesLeft()); - - if (packetLength == -1 && pesBuffer.bytesLeft() >= 6) { - // We haven't read the start of the packet, but have enough data to do so. - readPacketStart(); - } - if (packetLength > 0 && pesBuffer.bytesLeft() >= packetLength) { - // The packet length was specified and we now have the whole packet. Read it. - readPacketBody(); - } } - private void readPacketStart() { - pesBuffer.readBytes(scratch, 3); - int startCodePrefix = scratch.readBits(24); + private void setState(int state) { + this.state = state; + bytesRead = 0; + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read, or {@code null} to skip. + * @param targetLength The target length of the read. + * @return Whether the target length has been reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + if (bytesToRead <= 0) { + return true; + } else if (target == null) { + source.skip(bytesToRead); + } else { + source.readBytes(target, bytesRead, bytesToRead); + } + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + private boolean parseHeader() { + pesScratch.setPosition(0); + int startCodePrefix = pesScratch.readBits(24); if (startCodePrefix != 0x000001) { Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); - pesBuffer.reset(); - packetLength = -1; - } else { - pesBuffer.skip(1); // stream_id. - packetLength = pesBuffer.readUnsignedShort(); + payloadSize = -1; + return false; } + + pesScratch.skipBits(8); // stream_id. + int packetLength = pesScratch.readBits(16); + // First 8 bits are skipped: '10' (2), PES_scrambling_control (2), PES_priority (1), + // data_alignment_indicator (1), copyright (1), original_or_copy (1) + pesScratch.skipBits(8); + ptsFlag = pesScratch.readBit(); + // DTS_flag (1), ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1), + // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1) + pesScratch.skipBits(7); + extendedHeaderLength = pesScratch.readBits(8); + + if (packetLength == 0) { + payloadSize = -1; + } else { + payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */ + - HEADER_SIZE - extendedHeaderLength; + } + return true; } - private void readPacketBody() { - // '10' (2), PES_scrambling_control (2), PES_priority (1), data_alignment_indicator (1), - // copyright (1), original_or_copy (1) - pesBuffer.skip(1); - - pesBuffer.readBytes(scratch, 1); - boolean ptsFlag = scratch.readBit(); - // Last 7 bits of scratch are skipped: DTS_flag (1), ESCR_flag (1), ES_rate_flag (1), - // DSM_trick_mode_flag (1), additional_copy_info_flag (1), PES_CRC_flag (1), - // PES_extension_flag (1) - - int headerDataLength = pesBuffer.readUnsignedByte(); - if (headerDataLength == 0) { - headerDataLength = pesBuffer.bytesLeft(); - } - - long timeUs = 0; + private void parseHeaderExtension() { + pesScratch.setPosition(0); + timeUs = 0; if (ptsFlag) { - pesBuffer.readBytes(scratch, 5); - scratch.skipBits(4); // '0010' - long pts = scratch.readBitsLong(3) << 30; - scratch.skipBits(1); // marker_bit - pts |= scratch.readBitsLong(15) << 15; - scratch.skipBits(1); // marker_bit - pts |= scratch.readBitsLong(15); - scratch.skipBits(1); // marker_bit + pesScratch.skipBits(4); // '0010' + long pts = pesScratch.readBitsLong(3) << 30; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBitsLong(15) << 15; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBitsLong(15); + pesScratch.skipBits(1); // marker_bit timeUs = ptsToTimeUs(pts); - // Skip the rest of the header. - pesBuffer.skip(headerDataLength - 5); - } else { - // Skip the rest of the header. - pesBuffer.skip(headerDataLength); } - - int payloadSize; - if (packetLength == 0) { - // If pesPacketLength is not specified read all available data. - payloadSize = pesBuffer.bytesLeft(); - } else { - payloadSize = packetLength - headerDataLength - 3; - } - - pesPayloadReader.read(pesBuffer, payloadSize, timeUs); - pesBuffer.reset(); - packetLength = -1; } } @@ -781,7 +861,22 @@ public final class TsExtractor { internalQueue.add(sample); } - public abstract void read(ParsableByteArray pesBuffer, int pesPayloadSize, long pesTimeUs); + /** + * Consumes (possibly partial) payload data. + * + * @param data The payload data to consume. + * @param pesTimeUs The timestamp associated with the payload. + * @param startOfPacket True if this is the first time this method is being called for the + * current packet. False otherwise. + */ + public abstract void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket); + + /** + * Invoked once all of the payload data for a packet has been passed to + * {@link #consume(ParsableByteArray, long, boolean)}. The next call to + * {@link #consume(ParsableByteArray, long, boolean)} will have {@code startOfPacket == true}. + */ + public abstract void packetFinished(); } @@ -795,9 +890,8 @@ public final class TsExtractor { private static final int NAL_UNIT_TYPE_PPS = 8; private static final int NAL_UNIT_TYPE_AUD = 9; - public final SeiReader seiReader; + private final SeiReader seiReader; - // Used to store uncompleted sample data. private Sample currentSample; public H264Reader(SamplePool samplePool, SeiReader seiReader) { @@ -805,6 +899,27 @@ public final class TsExtractor { this.seiReader = seiReader; } + @Override + public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { + while (data.bytesLeft() > 0) { + if (readToNextAudUnit(data, pesTimeUs)) { + currentSample.isKeyframe = currentSample.size + > Mp4Util.findNalUnit(currentSample.data, 0, currentSample.size, NAL_UNIT_TYPE_IDR); + if (!hasMediaFormat() && currentSample.isKeyframe) { + parseMediaFormat(currentSample); + } + seiReader.read(currentSample.data, currentSample.size, currentSample.timeUs); + addSample(currentSample); + currentSample = null; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + @Override public void release() { super.release(); @@ -814,51 +929,37 @@ public final class TsExtractor { } } - @Override - public void read(ParsableByteArray pesBuffer, int pesPayloadSize, long pesTimeUs) { - // Read leftover frame data from previous PES packet. - pesPayloadSize -= readOneH264Frame(pesBuffer, true); + /** + * Reads data up to (but not including) the start of the next AUD unit. + * + * @param data The data to consume. + * @param pesTimeUs The corresponding time. + * @return True if the current sample is now complete. False otherwise. + */ + private boolean readToNextAudUnit(ParsableByteArray data, long pesTimeUs) { + int pesOffset = data.getPosition(); + int pesLimit = data.length(); - if (pesBuffer.bytesLeft() <= 0 || pesPayloadSize <= 0) { - return; - } - - // Single PES packet should contain only one new H.264 frame. - if (currentSample != null) { - if (!hasMediaFormat() && currentSample.isKeyframe) { - parseMediaFormat(currentSample); - } - seiReader.read(currentSample.data, currentSample.size, currentSample.timeUs); - addSample(currentSample); - } - currentSample = getSample(Sample.TYPE_VIDEO); - pesPayloadSize -= readOneH264Frame(pesBuffer, false); - currentSample.timeUs = pesTimeUs; - - if (pesPayloadSize > 0) { - Log.e(TAG, "PES packet contains more frame data than expected"); - } - } - - @SuppressLint("InlinedApi") - private int readOneH264Frame(ParsableByteArray pesBuffer, boolean remainderOnly) { - byte[] pesData = pesBuffer.data; - int pesOffset = pesBuffer.getPosition(); - int pesLimit = pesBuffer.length(); - - int searchOffset = pesOffset + (remainderOnly ? 0 : 3); - int audOffset = Mp4Util.findNalUnit(pesData, searchOffset, pesLimit, NAL_UNIT_TYPE_AUD); + // TODO: We probably need to handle the case where the AUD start code was split across the + // previous and current data buffers. + int audOffset = Mp4Util.findNalUnit(data.data, pesOffset, pesLimit, NAL_UNIT_TYPE_AUD); int bytesToNextAud = audOffset - pesOffset; - if (currentSample != null) { - int idrOffset = Mp4Util.findNalUnit(pesData, searchOffset, pesLimit, NAL_UNIT_TYPE_IDR); - if (idrOffset < audOffset) { - currentSample.isKeyframe = true; + if (bytesToNextAud == 0) { + if (currentSample == null) { + currentSample = getSample(Sample.TYPE_VIDEO); + currentSample.timeUs = pesTimeUs; + addToSample(currentSample, data, 4); + return false; + } else { + return true; } - addToSample(currentSample, pesBuffer, bytesToNextAud); + } else if (currentSample != null) { + addToSample(currentSample, data, bytesToNextAud); + return data.bytesLeft() > 0; } else { - pesBuffer.skip(bytesToNextAud); + data.skip(bytesToNextAud); + return false; } - return bytesToNextAud; } private void parseMediaFormat(Sample sample) { @@ -1086,56 +1187,142 @@ public final class TsExtractor { */ private class AdtsReader extends PesPayloadReader { - private final ParsableByteArray adtsBuffer; - private long timeUs; + private static final int STATE_FINDING_SYNC = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_SAMPLE = 2; + + private static final int HEADER_SIZE = 5; + private static final int CRC_SIZE = 2; + + private final ParsableBitArray adtsScratch; + + private int state; + private int bytesRead; + + // Used to find the header. + private boolean lastByteWasOxFF; + private boolean hasCrc; + + // Parsed from the header. private long frameDurationUs; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + private Sample currentSample; public AdtsReader(SamplePool samplePool) { super(samplePool); - adtsBuffer = new ParsableByteArray(); + adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); + state = STATE_FINDING_SYNC; } @Override - public void read(ParsableByteArray pesBuffer, int pesPayloadSize, long pesTimeUs) { - boolean needToProcessLeftOvers = !adtsBuffer.isEmpty(); - adtsBuffer.append(pesBuffer, pesPayloadSize); - // If there are leftovers from previous PES packet, process it with last calculated timeUs. - if (needToProcessLeftOvers && !readOneAacFrame(timeUs)) { - return; + public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { + if (startOfPacket) { + timeUs = pesTimeUs; + } + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC: + if (skipToNextSync(data)) { + bytesRead = 0; + state = STATE_READING_HEADER; + } + break; + case STATE_READING_HEADER: + int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; + if (continueRead(data, adtsScratch.getData(), targetLength)) { + parseHeader(); + currentSample = getSample(Sample.TYPE_AUDIO); + currentSample.timeUs = timeUs; + currentSample.isKeyframe = true; + bytesRead = 0; + state = STATE_READING_SAMPLE; + } + break; + case STATE_READING_SAMPLE: + int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + addToSample(currentSample, data, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + addSample(currentSample); + currentSample = null; + timeUs += frameDurationUs; + bytesRead = 0; + state = STATE_FINDING_SYNC; + } + break; + } } - int frameIndex = 0; - do { - timeUs = pesTimeUs + (frameDurationUs * frameIndex++); - } while(readOneAacFrame(timeUs)); } - @SuppressLint("InlinedApi") - private boolean readOneAacFrame(long timeUs) { - if (adtsBuffer.isEmpty()) { - return false; + @Override + public void packetFinished() { + // Do nothing. + } + + @Override + public void release() { + super.release(); + if (currentSample != null) { + recycle(currentSample); + currentSample = null; } + } - int offsetToSyncWord = findOffsetToSyncWord(); - adtsBuffer.skip(offsetToSyncWord); + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read. + * @param targetLength The target length of the read. + * @return Whether the target length was reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + source.readBytes(target, bytesRead, bytesToRead); + bytesRead += bytesToRead; + return bytesRead == targetLength; + } - int adtsStartOffset = adtsBuffer.getPosition(); - - if (adtsBuffer.bytesLeft() < 7) { - adtsBuffer.setPosition(adtsStartOffset); - adtsBuffer.clearReadData(); - return false; + /** + * Locates the next sync word, advancing the position to the byte that immediately follows it. + * If a sync word was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + * @return True if a sync word position was found. False otherwise. + */ + private boolean skipToNextSync(ParsableByteArray pesBuffer) { + byte[] adtsData = pesBuffer.data; + int startOffset = pesBuffer.getPosition(); + int endOffset = pesBuffer.length(); + for (int i = startOffset; i < endOffset; i++) { + boolean byteIsOxFF = (adtsData[i] & 0xFF) == 0xFF; + boolean found = lastByteWasOxFF && !byteIsOxFF && (adtsData[i] & 0xF0) == 0xF0; + lastByteWasOxFF = byteIsOxFF; + if (found) { + hasCrc = (adtsData[i] & 0x1) == 0; + pesBuffer.setPosition(i + 1); + return true; + } } + pesBuffer.setPosition(endOffset); + return false; + } - adtsBuffer.readBytes(scratch, 2); - scratch.skipBits(15); - boolean hasCRC = !scratch.readBit(); + /** + * Parses the sample header. + */ + private void parseHeader() { + adtsScratch.setPosition(0); - adtsBuffer.readBytes(scratch, 5); if (!hasMediaFormat()) { - int audioObjectType = scratch.readBits(2) + 1; - int sampleRateIndex = scratch.readBits(4); - scratch.skipBits(1); - int channelConfig = scratch.readBits(3); + int audioObjectType = adtsScratch.readBits(2) + 1; + int sampleRateIndex = adtsScratch.readBits(4); + adtsScratch.skipBits(1); + int channelConfig = adtsScratch.readBits(3); byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAudioSpecificConfig( audioObjectType, sampleRateIndex, channelConfig); @@ -1148,54 +1335,14 @@ public final class TsExtractor { frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate; setMediaFormat(mediaFormat); } else { - scratch.skipBits(10); - } - scratch.skipBits(4); - int frameSize = scratch.readBits(13); - scratch.skipBits(13); - - // Decrement frame size by ADTS header size and CRC. - if (hasCRC) { - // Skip CRC. - adtsBuffer.skip(2); - frameSize -= 9; - } else { - frameSize -= 7; + adtsScratch.skipBits(10); } - if (frameSize > adtsBuffer.bytesLeft()) { - adtsBuffer.setPosition(adtsStartOffset); - adtsBuffer.clearReadData(); - return false; + adtsScratch.skipBits(4); + sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE; + if (hasCrc) { + sampleSize -= CRC_SIZE; } - - addSample(Sample.TYPE_AUDIO, adtsBuffer, frameSize, timeUs, true); - return true; - } - - @Override - public void release() { - super.release(); - adtsBuffer.reset(); - } - - /** - * Finds the offset to the next Adts sync word. - * - * @return The position of the next Adts sync word. If an Adts sync word is not found, then the - * position of the end of the data is returned. - */ - private int findOffsetToSyncWord() { - byte[] adtsData = adtsBuffer.data; - int startOffset = adtsBuffer.getPosition(); - int endOffset = adtsBuffer.length(); - for (int i = startOffset; i < endOffset - 1; i++) { - int syncBits = ((adtsData[i] & 0xFF) << 8) | (adtsData[i + 1] & 0xFF); - if ((syncBits & 0xFFF0) == 0xFFF0 && syncBits != 0xFFFF) { - return i - startOffset; - } - } - return endOffset - startOffset; } } @@ -1205,6 +1352,8 @@ public final class TsExtractor { */ private class Id3Reader extends PesPayloadReader { + private Sample currentSample; + public Id3Reader(SamplePool samplePool) { super(samplePool); setMediaFormat(MediaFormat.createId3Format()); @@ -1212,8 +1361,30 @@ public final class TsExtractor { @SuppressLint("InlinedApi") @Override - public void read(ParsableByteArray pesBuffer, int pesPayloadSize, long pesTimeUs) { - addSample(Sample.TYPE_MISC, pesBuffer, pesPayloadSize, pesTimeUs, true); + public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { + if (startOfPacket) { + currentSample = getSample(Sample.TYPE_MISC); + currentSample.timeUs = pesTimeUs; + currentSample.isKeyframe = true; + } + if (currentSample != null) { + addToSample(currentSample, data, data.bytesLeft()); + } + } + + @Override + public void packetFinished() { + addSample(currentSample); + currentSample = null; + } + + @Override + public void release() { + super.release(); + if (currentSample != null) { + recycle(currentSample); + currentSample = null; + } } } diff --git a/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java index 98b4e2aa07..5038d4e1d6 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer.util; import java.nio.ByteBuffer; -import java.util.Arrays; /** * Wraps a byte array, providing a set of methods for parsing data from it. Numerical values are @@ -69,39 +68,27 @@ public final class ParsableByteArray { limit = 0; } - // TODO: This method is temporary - public void append(ParsableByteArray buffer, int length) { - if (data == null) { - data = new byte[length]; - } else if (data.length < limit + length) { - data = Arrays.copyOf(data, limit + length); - } - buffer.readBytes(data, limit, length); - limit += length; - } - - // TODO: This method is temporary - public void clearReadData() { - System.arraycopy(data, position, data, 0, limit - position); - limit -= position; - position = 0; - } - - // TODO: This method is temporary - public boolean isEmpty() { - return limit == 0; - } - /** Returns the number of bytes yet to be read. */ public int bytesLeft() { return limit - position; } /** Returns the number of bytes in the array. */ + // TODO: Rename to limit. public int length() { return limit; } + /** + * Sets the limit. + * + * @param limit The limit to set. + */ + public void setLimit(int limit) { + Assertions.checkArgument(limit >= 0 && limit <= data.length); + this.limit = limit; + } + /** Returns the current offset in the array, in bytes. */ public int getPosition() { return position; From 3568ecaf001a792441e36fca22b784ee1463deb3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 12 Feb 2015 12:09:57 +0000 Subject: [PATCH 04/17] Split TsExtractor into multiple files. There's no code change here at all, except for how TsExtractor's getLargestSampleTimestamp method works. --- .../android/exoplayer/hls/HlsChunkSource.java | 5 +- .../exoplayer/hls/HlsSampleSource.java | 1 + .../google/android/exoplayer/hls/TsChunk.java | 1 + .../android/exoplayer/hls/TsExtractor.java | 1471 ----------------- .../exoplayer/hls/parser/AdtsReader.java | 192 +++ .../exoplayer/hls/parser/H264Reader.java | 266 +++ .../exoplayer/hls/parser/Id3Reader.java | 63 + .../hls/parser/PesPayloadReader.java | 66 + .../android/exoplayer/hls/parser/Sample.java | 55 + .../exoplayer/hls/parser/SamplePool.java | 58 + .../exoplayer/hls/parser/SampleQueue.java | 221 +++ .../exoplayer/hls/parser/SeiReader.java | 87 + .../exoplayer/hls/parser/TsExtractor.java | 629 +++++++ 13 files changed, 1642 insertions(+), 1473 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/Sample.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/SamplePool.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java 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 5f8ccaef89..d7f5beef2d 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,7 +17,8 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.hls.TsExtractor.SamplePool; +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.DataSource; @@ -101,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 TsExtractor.SamplePool(); + private final SamplePool samplePool = new SamplePool(); private final DataSource upstreamDataSource; private final HlsPlaylistParser playlistParser; private final Variant[] enabledVariants; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index ea6fddd488..4603577ff9 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.hls.parser.TsExtractor; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.util.Assertions; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java index 04a3f9200f..36c1e30c8f 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.hls; +import com.google.android.exoplayer.hls.parser.TsExtractor; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java deleted file mode 100644 index ae9ec2159f..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ /dev/null @@ -1,1471 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer.hls; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.mp4.Mp4Util; -import com.google.android.exoplayer.text.eia608.Eia608Parser; -import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.CodecSpecificDataUtil; -import com.google.android.exoplayer.util.MimeTypes; -import com.google.android.exoplayer.util.ParsableBitArray; -import com.google.android.exoplayer.util.ParsableByteArray; - -import android.annotation.SuppressLint; -import android.media.MediaExtractor; -import android.util.Log; -import android.util.Pair; -import android.util.SparseArray; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.TreeSet; -import java.util.concurrent.ConcurrentLinkedQueue; - -/** - * Facilitates the extraction of data from the MPEG-2 TS container format. - */ -public final class TsExtractor { - - private static final String TAG = "TsExtractor"; - - private static final int TS_PACKET_SIZE = 188; - private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. - private static final int TS_PAT_PID = 0; - - private static final int TS_STREAM_TYPE_AAC = 0x0F; - private static final int TS_STREAM_TYPE_H264 = 0x1B; - private static final int TS_STREAM_TYPE_ID3 = 0x15; - private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 - - private static final long MAX_PTS = 0x1FFFFFFFFL; - - private final ParsableByteArray tsPacketBuffer; - private final SparseArray sampleQueues; // Indexed by streamType - private final SparseArray tsPayloadReaders; // Indexed by pid - private final SamplePool samplePool; - private final boolean shouldSpliceIn; - private final long firstSampleTimestamp; - private final ParsableBitArray tsScratch; - - // Accessed only by the consuming thread. - private boolean spliceConfigured; - - // Accessed only by the loading thread. - private int tsPacketBytesRead; - private long timestampOffsetUs; - private long lastPts; - - // Accessed by both the loading and consuming threads. - private volatile boolean prepared; - /* package */ volatile long largestParsedTimestampUs; - - public TsExtractor(long firstSampleTimestamp, SamplePool samplePool, boolean shouldSpliceIn) { - this.firstSampleTimestamp = firstSampleTimestamp; - this.samplePool = samplePool; - this.shouldSpliceIn = shouldSpliceIn; - tsScratch = new ParsableBitArray(new byte[3]); - tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); - sampleQueues = new SparseArray(); - tsPayloadReaders = new SparseArray(); - tsPayloadReaders.put(TS_PAT_PID, new PatReader()); - largestParsedTimestampUs = Long.MIN_VALUE; - lastPts = Long.MIN_VALUE; - } - - /** - * Gets the number of available tracks. - *

- * This method should only be called after the extractor has been prepared. - * - * @return The number of available tracks. - */ - public int getTrackCount() { - Assertions.checkState(prepared); - return sampleQueues.size(); - } - - /** - * Gets the format of the specified track. - *

- * This method must only be called after the extractor has been prepared. - * - * @param track The track index. - * @return The corresponding format. - */ - public MediaFormat getFormat(int track) { - Assertions.checkState(prepared); - return sampleQueues.valueAt(track).getMediaFormat(); - } - - /** - * Whether the extractor is prepared. - * - * @return True if the extractor is prepared. False otherwise. - */ - public boolean isPrepared() { - return prepared; - } - - /** - * Releases the extractor, recycling any pending or incomplete samples to the sample pool. - *

- * This method should not be called whilst {@link #read(DataSource)} is also being invoked. - */ - public void release() { - for (int i = 0; i < sampleQueues.size(); i++) { - sampleQueues.valueAt(i).release(); - } - } - - /** - * Attempts to configure a splice from this extractor to the next. - *

- * The splice is performed such that for each track the samples read from the next extractor - * start with a keyframe, and continue from where the samples read from this extractor finish. - * A successful splice may discard samples from either or both extractors. - *

- * Splice configuration may fail if the next extractor is not yet in a state that allows the - * splice to be performed. Calling this method is a noop if the splice has already been - * configured. Hence this method should be called repeatedly during the window within which a - * splice can be performed. - * - * @param nextExtractor The extractor being spliced to. - */ - public void configureSpliceTo(TsExtractor nextExtractor) { - Assertions.checkState(prepared); - if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) { - // The splice is already configured, or the next extractor doesn't want to be spliced in, or - // the next extractor isn't ready to be spliced in. - return; - } - boolean spliceConfigured = true; - for (int i = 0; i < sampleQueues.size(); i++) { - spliceConfigured &= sampleQueues.valueAt(i).configureSpliceTo( - nextExtractor.sampleQueues.valueAt(i)); - } - this.spliceConfigured = spliceConfigured; - return; - } - - /** - * Gets the largest timestamp of any sample parsed by the extractor. - * - * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed. - */ - public long getLargestSampleTimestamp() { - return largestParsedTimestampUs; - } - - /** - * Gets the next sample for the specified track. - * - * @param track The track from which to read. - * @param out A {@link SampleHolder} into which the next sample should be read. - * @return True if a sample was read. False otherwise. - */ - public boolean getSample(int track, SampleHolder out) { - Assertions.checkState(prepared); - SampleQueue sampleQueue = sampleQueues.valueAt(track); - Sample sample = sampleQueue.poll(); - if (sample == null) { - return false; - } - convert(sample, out); - sampleQueue.recycle(sample); - return true; - } - - /** - * Discards samples for the specified track up to the specified time. - * - * @param track The track from which samples should be discarded. - * @param timeUs The time up to which samples should be discarded, in microseconds. - */ - public void discardUntil(int track, long timeUs) { - Assertions.checkState(prepared); - sampleQueues.valueAt(track).discardUntil(timeUs); - } - - /** - * Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the - * specified track. - * - * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)} - * for the specified track. False otherwise. - */ - public boolean hasSamples(int track) { - Assertions.checkState(prepared); - return sampleQueues.valueAt(track).peek() != null; - } - - private boolean checkPrepared() { - int pesPayloadReaderCount = sampleQueues.size(); - if (pesPayloadReaderCount == 0) { - return false; - } - for (int i = 0; i < pesPayloadReaderCount; i++) { - if (!sampleQueues.valueAt(i).hasMediaFormat()) { - return false; - } - } - return true; - } - - /** - * Reads up to a single TS packet. - * - * @param dataSource The {@link DataSource} from which to read. - * @throws IOException If an error occurred reading from the source. - * @return The number of bytes read from the source. - */ - public int read(DataSource dataSource) throws IOException { - int bytesRead = dataSource.read(tsPacketBuffer.data, tsPacketBytesRead, - TS_PACKET_SIZE - tsPacketBytesRead); - if (bytesRead == -1) { - return -1; - } - - tsPacketBytesRead += bytesRead; - if (tsPacketBytesRead < TS_PACKET_SIZE) { - // We haven't read the whole packet yet. - return bytesRead; - } - - // Reset before reading the packet. - tsPacketBytesRead = 0; - tsPacketBuffer.setPosition(0); - tsPacketBuffer.setLimit(TS_PACKET_SIZE); - - int syncByte = tsPacketBuffer.readUnsignedByte(); - if (syncByte != TS_SYNC_BYTE) { - return bytesRead; - } - - tsPacketBuffer.readBytes(tsScratch, 3); - tsScratch.skipBits(1); // transport_error_indicator - boolean payloadUnitStartIndicator = tsScratch.readBit(); - tsScratch.skipBits(1); // transport_priority - int pid = tsScratch.readBits(13); - tsScratch.skipBits(2); // transport_scrambling_control - boolean adaptationFieldExists = tsScratch.readBit(); - boolean payloadExists = tsScratch.readBit(); - // Last 4 bits of scratch are skipped: continuity_counter - - // Skip the adaptation field. - if (adaptationFieldExists) { - int adaptationFieldLength = tsPacketBuffer.readUnsignedByte(); - tsPacketBuffer.skip(adaptationFieldLength); - } - - // Read the payload. - if (payloadExists) { - TsPayloadReader payloadReader = tsPayloadReaders.get(pid); - if (payloadReader != null) { - payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); - } - } - - if (!prepared) { - prepared = checkPrepared(); - } - - return bytesRead; - } - - @SuppressLint("InlinedApi") - private void convert(Sample in, SampleHolder out) { - if (out.data == null || out.data.capacity() < in.size) { - out.replaceBuffer(in.size); - } - if (out.data != null) { - out.data.put(in.data, 0, in.size); - } - out.size = in.size; - out.flags = in.isKeyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; - out.timeUs = in.timeUs; - } - - /** - * Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound. - * - * @param pts The raw PTS value. - * @return The corresponding time in microseconds. - */ - /* package */ long ptsToTimeUs(long pts) { - if (lastPts != Long.MIN_VALUE) { - // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), - // and we need to snap to the one closest to lastPts. - long closestWrapCount = (lastPts + (MAX_PTS / 2)) / MAX_PTS; - long ptsWrapBelow = pts + (MAX_PTS * (closestWrapCount - 1)); - long ptsWrapAbove = pts + (MAX_PTS * closestWrapCount); - pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts) - ? ptsWrapBelow : ptsWrapAbove; - } - // Calculate the corresponding timestamp. - long timeUs = (pts * C.MICROS_PER_SECOND) / 90000; - // If we haven't done the initial timestamp adjustment, do it now. - if (lastPts == Long.MIN_VALUE) { - timestampOffsetUs = firstSampleTimestamp - timeUs; - } - // Record the adjusted PTS to adjust for wraparound next time. - lastPts = pts; - return timeUs + timestampOffsetUs; - } - - /** - * Parses TS packet payload data. - */ - private abstract static class TsPayloadReader { - - public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator); - - } - - /** - * Parses Program Association Table data. - */ - private class PatReader extends TsPayloadReader { - - private final ParsableBitArray patScratch; - - public PatReader() { - patScratch = new ParsableBitArray(new byte[4]); - } - - @Override - public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { - // Skip pointer. - if (payloadUnitStartIndicator) { - int pointerField = data.readUnsignedByte(); - data.skip(pointerField); - } - - data.readBytes(patScratch, 3); - patScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) - int sectionLength = patScratch.readBits(12); - // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1), - // section_number (8), last_section_number (8) - data.skip(5); - - int programCount = (sectionLength - 9) / 4; - for (int i = 0; i < programCount; i++) { - data.readBytes(patScratch, 4); - patScratch.skipBits(19); // program_number (16), reserved (3) - int pid = patScratch.readBits(13); - tsPayloadReaders.put(pid, new PmtReader()); - } - - // Skip CRC_32. - } - - } - - /** - * Parses Program Map Table. - */ - private class PmtReader extends TsPayloadReader { - - private final ParsableBitArray pmtScratch; - - public PmtReader() { - pmtScratch = new ParsableBitArray(new byte[5]); - } - - @Override - public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { - // Skip pointer. - if (payloadUnitStartIndicator) { - int pointerField = data.readUnsignedByte(); - data.skip(pointerField); - } - - data.readBytes(pmtScratch, 3); - pmtScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) - int sectionLength = pmtScratch.readBits(12); - - // program_number (16), reserved (2), version_number (5), current_next_indicator (1), - // section_number (8), last_section_number (8), reserved (3), PCR_PID (13) - // Skip the rest of the PMT header. - data.skip(7); - - data.readBytes(pmtScratch, 2); - pmtScratch.skipBits(4); - int programInfoLength = pmtScratch.readBits(12); - - // Skip the descriptors. - data.skip(programInfoLength); - - int entriesSize = sectionLength - 9 /* Size of the rest of the fields before descriptors */ - - programInfoLength - 4 /* CRC size */; - while (entriesSize > 0) { - data.readBytes(pmtScratch, 5); - int streamType = pmtScratch.readBits(8); - pmtScratch.skipBits(3); // reserved - int elementaryPid = pmtScratch.readBits(13); - pmtScratch.skipBits(4); // reserved - int esInfoLength = pmtScratch.readBits(12); - - // Skip the descriptors. - data.skip(esInfoLength); - entriesSize -= esInfoLength + 5; - - if (sampleQueues.get(streamType) != null) { - continue; - } - - PesPayloadReader pesPayloadReader = null; - switch (streamType) { - case TS_STREAM_TYPE_AAC: - pesPayloadReader = new AdtsReader(samplePool); - break; - case TS_STREAM_TYPE_H264: - SeiReader seiReader = new SeiReader(samplePool); - sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader); - pesPayloadReader = new H264Reader(samplePool, seiReader); - break; - case TS_STREAM_TYPE_ID3: - pesPayloadReader = new Id3Reader(samplePool); - break; - } - - if (pesPayloadReader != null) { - sampleQueues.put(streamType, pesPayloadReader); - tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader)); - } - } - - // Skip CRC_32. - } - - } - - /** - * Parses PES packet data and extracts samples. - */ - private class PesReader extends TsPayloadReader { - - private static final int STATE_FINDING_HEADER = 0; - private static final int STATE_READING_HEADER = 1; - private static final int STATE_READING_HEADER_EXTENSION = 2; - private static final int STATE_READING_BODY = 3; - - private static final int HEADER_SIZE = 9; - private static final int MAX_HEADER_EXTENSION_SIZE = 5; - - private final ParsableBitArray pesScratch; - private final PesPayloadReader pesPayloadReader; - - private int state; - private int bytesRead; - private boolean bodyStarted; - - private boolean ptsFlag; - private int extendedHeaderLength; - - private int payloadSize; - - private long timeUs; - - public PesReader(PesPayloadReader pesPayloadReader) { - this.pesPayloadReader = pesPayloadReader; - pesScratch = new ParsableBitArray(new byte[HEADER_SIZE]); - state = STATE_FINDING_HEADER; - } - - @Override - public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { - if (payloadUnitStartIndicator) { - switch (state) { - case STATE_FINDING_HEADER: - case STATE_READING_HEADER: - // Expected. - break; - case STATE_READING_HEADER_EXTENSION: - Log.w(TAG, "Unexpected start indicator reading extended header"); - break; - case STATE_READING_BODY: - // If payloadSize == -1 then the length of the previous packet was unspecified, and so - // we only know that it's finished now that we've seen the start of the next one. This - // is expected. If payloadSize != -1, then the length of the previous packet was known, - // but we didn't receive that amount of data. This is not expected. - if (payloadSize != -1) { - Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes"); - } - // Either way, if the body was started, notify the reader that it has now finished. - if (bodyStarted) { - pesPayloadReader.packetFinished(); - } - break; - } - setState(STATE_READING_HEADER); - } - - while (data.bytesLeft() > 0) { - switch (state) { - case STATE_FINDING_HEADER: - data.skip(data.bytesLeft()); - break; - case STATE_READING_HEADER: - if (continueRead(data, pesScratch.getData(), HEADER_SIZE)) { - setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER); - } - break; - case STATE_READING_HEADER_EXTENSION: - int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); - // Read as much of the extended header as we're interested in, and skip the rest. - if (continueRead(data, pesScratch.getData(), readLength) - && continueRead(data, null, extendedHeaderLength)) { - parseHeaderExtension(); - bodyStarted = false; - setState(STATE_READING_BODY); - } - break; - case STATE_READING_BODY: - readLength = data.bytesLeft(); - int padding = payloadSize == -1 ? 0 : readLength - payloadSize; - if (padding > 0) { - readLength -= padding; - data.setLimit(data.getPosition() + readLength); - } - pesPayloadReader.consume(data, timeUs, !bodyStarted); - bodyStarted = true; - if (payloadSize != -1) { - payloadSize -= readLength; - if (payloadSize == 0) { - pesPayloadReader.packetFinished(); - setState(STATE_READING_HEADER); - } - } - break; - } - } - } - - private void setState(int state) { - this.state = state; - bytesRead = 0; - } - - /** - * Continues a read from the provided {@code source} into a given {@code target}. It's assumed - * that the data should be written into {@code target} starting from an offset of zero. - * - * @param source The source from which to read. - * @param target The target into which data is to be read, or {@code null} to skip. - * @param targetLength The target length of the read. - * @return Whether the target length has been reached. - */ - private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { - int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); - if (bytesToRead <= 0) { - return true; - } else if (target == null) { - source.skip(bytesToRead); - } else { - source.readBytes(target, bytesRead, bytesToRead); - } - bytesRead += bytesToRead; - return bytesRead == targetLength; - } - - private boolean parseHeader() { - pesScratch.setPosition(0); - int startCodePrefix = pesScratch.readBits(24); - if (startCodePrefix != 0x000001) { - Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); - payloadSize = -1; - return false; - } - - pesScratch.skipBits(8); // stream_id. - int packetLength = pesScratch.readBits(16); - // First 8 bits are skipped: '10' (2), PES_scrambling_control (2), PES_priority (1), - // data_alignment_indicator (1), copyright (1), original_or_copy (1) - pesScratch.skipBits(8); - ptsFlag = pesScratch.readBit(); - // DTS_flag (1), ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1), - // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1) - pesScratch.skipBits(7); - extendedHeaderLength = pesScratch.readBits(8); - - if (packetLength == 0) { - payloadSize = -1; - } else { - payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */ - - HEADER_SIZE - extendedHeaderLength; - } - return true; - } - - private void parseHeaderExtension() { - pesScratch.setPosition(0); - timeUs = 0; - if (ptsFlag) { - pesScratch.skipBits(4); // '0010' - long pts = pesScratch.readBitsLong(3) << 30; - pesScratch.skipBits(1); // marker_bit - pts |= pesScratch.readBitsLong(15) << 15; - pesScratch.skipBits(1); // marker_bit - pts |= pesScratch.readBitsLong(15); - pesScratch.skipBits(1); // marker_bit - timeUs = ptsToTimeUs(pts); - } - } - - } - - /** - * A queue of extracted samples together with their corresponding {@link MediaFormat}. - */ - private abstract class SampleQueue { - - @SuppressWarnings("hiding") - private final SamplePool samplePool; - - // Accessed only by the consuming thread. - private boolean needKeyframe; - private long lastReadTimeUs; - private long spliceOutTimeUs; - - // Accessed by both the loading and consuming threads. - private volatile MediaFormat mediaFormat; - - protected SampleQueue(SamplePool samplePool) { - this.samplePool = samplePool; - needKeyframe = true; - lastReadTimeUs = Long.MIN_VALUE; - spliceOutTimeUs = Long.MIN_VALUE; - } - - public boolean hasMediaFormat() { - return mediaFormat != null; - } - - public MediaFormat getMediaFormat() { - return mediaFormat; - } - - protected void setMediaFormat(MediaFormat mediaFormat) { - this.mediaFormat = mediaFormat; - } - - /** - * Removes and returns the next sample from the queue. - *

- * The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples - * queued prior to the first keyframe are discarded. - * - * @return The next sample from the queue, or null if a sample isn't available. - */ - public Sample poll() { - Sample head = peek(); - if (head != null) { - internalPollSample(); - needKeyframe = false; - lastReadTimeUs = head.timeUs; - } - return head; - } - - /** - * Like {@link #poll()}, except the returned sample is not removed from the queue. - * - * @return The next sample from the queue, or null if a sample isn't available. - */ - public Sample peek() { - Sample head = internalPeekSample(); - if (needKeyframe) { - // Peeking discard of samples until we find a keyframe or run out of available samples. - while (head != null && !head.isKeyframe) { - recycle(head); - internalPollSample(); - head = internalPeekSample(); - } - } - 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. - recycle(head); - internalPollSample(); - 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) { - recycle(head); - internalPollSample(); - head = internalPeekSample(); - // We're discarding at least one sample, so any subsequent read will need to start at - // a keyframe. - needKeyframe = true; - } - lastReadTimeUs = Long.MIN_VALUE; - } - - /** - * Clears the queue. - */ - public void release() { - Sample toRecycle = internalPollSample(); - while (toRecycle != null) { - recycle(toRecycle); - toRecycle = internalPollSample(); - } - } - - /** - * Recycles a sample. - * - * @param sample The sample to recycle. - */ - public void recycle(Sample sample) { - samplePool.recycle(sample); - } - - /** - * Attempts to configure a splice from this queue to the next. - * - * @param nextQueue The queue being spliced to. - * @return Whether the splice was configured successfully. - */ - public boolean configureSpliceTo(SampleQueue nextQueue) { - if (spliceOutTimeUs != Long.MIN_VALUE) { - // We've already configured the splice. - return true; - } - long firstPossibleSpliceTime; - Sample nextSample = internalPeekSample(); - if (nextSample != null) { - firstPossibleSpliceTime = nextSample.timeUs; - } else { - firstPossibleSpliceTime = lastReadTimeUs + 1; - } - Sample nextQueueSample = nextQueue.internalPeekSample(); - while (nextQueueSample != null - && (nextQueueSample.timeUs < firstPossibleSpliceTime || !nextQueueSample.isKeyframe)) { - // Discard samples from the next queue for as long as they are before the earliest possible - // splice time, or not keyframes. - nextQueue.internalPollSample(); - nextQueueSample = nextQueue.internalPeekSample(); - } - if (nextQueueSample != null) { - // We've found a keyframe in the next queue that can serve as the splice point. Set the - // splice point now. - spliceOutTimeUs = nextQueueSample.timeUs; - return true; - } - return false; - } - - /** - * Obtains a Sample object to use. - * - * @param type The type of the sample. - * @return The sample. - */ - protected Sample getSample(int type) { - return samplePool.get(type); - } - - /** - * Creates a new Sample and adds it to the queue. - * - * @param type The type of the sample. - * @param buffer The buffer to read sample data. - * @param sampleSize The size of the sample data. - * @param sampleTimeUs The sample time stamp. - * @param isKeyframe True if the sample is a keyframe. False otherwise. - */ - protected void addSample(int type, ParsableByteArray buffer, int sampleSize, long sampleTimeUs, - boolean isKeyframe) { - Sample sample = getSample(type); - addToSample(sample, buffer, sampleSize); - sample.isKeyframe = isKeyframe; - sample.timeUs = sampleTimeUs; - addSample(sample); - } - - protected void addSample(Sample sample) { - largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); - internalQueueSample(sample); - } - - protected void addToSample(Sample sample, ParsableByteArray buffer, int size) { - if (sample.data.length - sample.size < size) { - sample.expand(size - sample.data.length + sample.size); - } - buffer.readBytes(sample.data, sample.size, size); - sample.size += size; - } - - protected abstract Sample internalPeekSample(); - protected abstract Sample internalPollSample(); - protected abstract void internalQueueSample(Sample sample); - - } - - /** - * Extracts individual samples from continuous byte stream, preserving original order. - */ - private abstract class PesPayloadReader extends SampleQueue { - - private final ConcurrentLinkedQueue internalQueue; - - protected PesPayloadReader(SamplePool samplePool) { - super(samplePool); - internalQueue = new ConcurrentLinkedQueue(); - } - - @Override - protected final Sample internalPeekSample() { - return internalQueue.peek(); - } - - @Override - protected final Sample internalPollSample() { - return internalQueue.poll(); - } - - @Override - protected final void internalQueueSample(Sample sample) { - internalQueue.add(sample); - } - - /** - * Consumes (possibly partial) payload data. - * - * @param data The payload data to consume. - * @param pesTimeUs The timestamp associated with the payload. - * @param startOfPacket True if this is the first time this method is being called for the - * current packet. False otherwise. - */ - public abstract void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket); - - /** - * Invoked once all of the payload data for a packet has been passed to - * {@link #consume(ParsableByteArray, long, boolean)}. The next call to - * {@link #consume(ParsableByteArray, long, boolean)} will have {@code startOfPacket == true}. - */ - public abstract void packetFinished(); - - } - - /** - * Parses a continuous H264 byte stream and extracts individual frames. - */ - private class H264Reader extends PesPayloadReader { - - private static final int NAL_UNIT_TYPE_IDR = 5; - private static final int NAL_UNIT_TYPE_SPS = 7; - private static final int NAL_UNIT_TYPE_PPS = 8; - private static final int NAL_UNIT_TYPE_AUD = 9; - - private final SeiReader seiReader; - - private Sample currentSample; - - public H264Reader(SamplePool samplePool, SeiReader seiReader) { - super(samplePool); - this.seiReader = seiReader; - } - - @Override - public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { - while (data.bytesLeft() > 0) { - if (readToNextAudUnit(data, pesTimeUs)) { - currentSample.isKeyframe = currentSample.size - > Mp4Util.findNalUnit(currentSample.data, 0, currentSample.size, NAL_UNIT_TYPE_IDR); - if (!hasMediaFormat() && currentSample.isKeyframe) { - parseMediaFormat(currentSample); - } - seiReader.read(currentSample.data, currentSample.size, currentSample.timeUs); - addSample(currentSample); - currentSample = null; - } - } - } - - @Override - public void packetFinished() { - // Do nothing. - } - - @Override - public void release() { - super.release(); - if (currentSample != null) { - recycle(currentSample); - currentSample = null; - } - } - - /** - * Reads data up to (but not including) the start of the next AUD unit. - * - * @param data The data to consume. - * @param pesTimeUs The corresponding time. - * @return True if the current sample is now complete. False otherwise. - */ - private boolean readToNextAudUnit(ParsableByteArray data, long pesTimeUs) { - int pesOffset = data.getPosition(); - int pesLimit = data.length(); - - // TODO: We probably need to handle the case where the AUD start code was split across the - // previous and current data buffers. - int audOffset = Mp4Util.findNalUnit(data.data, pesOffset, pesLimit, NAL_UNIT_TYPE_AUD); - int bytesToNextAud = audOffset - pesOffset; - if (bytesToNextAud == 0) { - if (currentSample == null) { - currentSample = getSample(Sample.TYPE_VIDEO); - currentSample.timeUs = pesTimeUs; - addToSample(currentSample, data, 4); - return false; - } else { - return true; - } - } else if (currentSample != null) { - addToSample(currentSample, data, bytesToNextAud); - return data.bytesLeft() > 0; - } else { - data.skip(bytesToNextAud); - return false; - } - } - - private void parseMediaFormat(Sample sample) { - byte[] sampleData = sample.data; - int sampleSize = sample.size; - // Locate the SPS and PPS units. - int spsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_SPS); - int ppsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_PPS); - if (spsOffset == sampleSize || ppsOffset == sampleSize) { - return; - } - // Determine the length of the units, and copy them to build the initialization data. - int spsLength = Mp4Util.findNalUnit(sampleData, spsOffset + 3, sampleSize) - spsOffset; - int ppsLength = Mp4Util.findNalUnit(sampleData, ppsOffset + 3, sampleSize) - ppsOffset; - byte[] spsData = new byte[spsLength]; - byte[] ppsData = new byte[ppsLength]; - System.arraycopy(sampleData, spsOffset, spsData, 0, spsLength); - System.arraycopy(sampleData, ppsOffset, ppsData, 0, ppsLength); - List initializationData = new ArrayList(); - initializationData.add(spsData); - initializationData.add(ppsData); - - // Unescape and then parse the SPS unit. - byte[] unescapedSps = unescapeStream(spsData, 0, spsLength); - ParsableBitArray bitArray = new ParsableBitArray(unescapedSps); - bitArray.skipBits(32); // NAL header - int profileIdc = bitArray.readBits(8); - bitArray.skipBits(16); // constraint bits (6), reserved (2) and level_idc (8) - bitArray.readUnsignedExpGolombCodedInt(); // seq_parameter_set_id - - int chromaFormatIdc = 1; // Default is 4:2:0 - if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244 - || profileIdc == 44 || profileIdc == 83 || profileIdc == 86 || profileIdc == 118 - || profileIdc == 128 || profileIdc == 138) { - chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); - if (chromaFormatIdc == 3) { - bitArray.skipBits(1); // separate_colour_plane_flag - } - bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8 - bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8 - bitArray.skipBits(1); // qpprime_y_zero_transform_bypass_flag - boolean seqScalingMatrixPresentFlag = bitArray.readBit(); - if (seqScalingMatrixPresentFlag) { - int limit = (chromaFormatIdc != 3) ? 8 : 12; - for (int i = 0; i < limit; i++) { - boolean seqScalingListPresentFlag = bitArray.readBit(); - if (seqScalingListPresentFlag) { - skipScalingList(bitArray, i < 6 ? 16 : 64); - } - } - } - } - - bitArray.readUnsignedExpGolombCodedInt(); // log2_max_frame_num_minus4 - long picOrderCntType = bitArray.readUnsignedExpGolombCodedInt(); - if (picOrderCntType == 0) { - bitArray.readUnsignedExpGolombCodedInt(); // log2_max_pic_order_cnt_lsb_minus4 - } else if (picOrderCntType == 1) { - bitArray.skipBits(1); // delta_pic_order_always_zero_flag - bitArray.readSignedExpGolombCodedInt(); // offset_for_non_ref_pic - bitArray.readSignedExpGolombCodedInt(); // offset_for_top_to_bottom_field - long numRefFramesInPicOrderCntCycle = bitArray.readUnsignedExpGolombCodedInt(); - for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) { - bitArray.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i] - } - } - bitArray.readUnsignedExpGolombCodedInt(); // max_num_ref_frames - bitArray.skipBits(1); // gaps_in_frame_num_value_allowed_flag - - int picWidthInMbs = bitArray.readUnsignedExpGolombCodedInt() + 1; - int picHeightInMapUnits = bitArray.readUnsignedExpGolombCodedInt() + 1; - boolean frameMbsOnlyFlag = bitArray.readBit(); - int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits; - if (!frameMbsOnlyFlag) { - bitArray.skipBits(1); // mb_adaptive_frame_field_flag - } - - bitArray.skipBits(1); // direct_8x8_inference_flag - int frameWidth = picWidthInMbs * 16; - int frameHeight = frameHeightInMbs * 16; - boolean frameCroppingFlag = bitArray.readBit(); - if (frameCroppingFlag) { - int frameCropLeftOffset = bitArray.readUnsignedExpGolombCodedInt(); - int frameCropRightOffset = bitArray.readUnsignedExpGolombCodedInt(); - int frameCropTopOffset = bitArray.readUnsignedExpGolombCodedInt(); - int frameCropBottomOffset = bitArray.readUnsignedExpGolombCodedInt(); - int cropUnitX, cropUnitY; - if (chromaFormatIdc == 0) { - cropUnitX = 1; - cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0); - } else { - int subWidthC = (chromaFormatIdc == 3) ? 1 : 2; - int subHeightC = (chromaFormatIdc == 1) ? 2 : 1; - cropUnitX = subWidthC; - cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0)); - } - frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX; - frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY; - } - - // Set the format. - setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, - frameWidth, frameHeight, initializationData)); - } - - private void skipScalingList(ParsableBitArray bitArray, int size) { - int lastScale = 8; - int nextScale = 8; - for (int i = 0; i < size; i++) { - if (nextScale != 0) { - int deltaScale = bitArray.readSignedExpGolombCodedInt(); - nextScale = (lastScale + deltaScale + 256) % 256; - } - lastScale = (nextScale == 0) ? lastScale : nextScale; - } - } - - /** - * Replaces occurrences of [0, 0, 3] with [0, 0]. - *

- * See ISO/IEC 14496-10:2005(E) page 36 for more information. - */ - private byte[] unescapeStream(byte[] data, int offset, int limit) { - int position = offset; - List escapePositions = new ArrayList(); - while (position < limit) { - position = findNextUnescapeIndex(data, position, limit); - if (position < limit) { - escapePositions.add(position); - position += 3; - } - } - - int escapeCount = escapePositions.size(); - int escapedPosition = offset; // The position being read from. - int unescapedPosition = 0; // The position being written to. - byte[] unescapedData = new byte[limit - offset - escapeCount]; - for (int i = 0; i < escapeCount; i++) { - int nextEscapePosition = escapePositions.get(i); - int copyLength = nextEscapePosition - escapedPosition; - System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, copyLength); - escapedPosition += copyLength + 3; - unescapedPosition += copyLength + 2; - } - - int remainingLength = unescapedData.length - unescapedPosition; - System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, remainingLength); - return unescapedData; - } - - private int findNextUnescapeIndex(byte[] bytes, int offset, int limit) { - for (int i = offset; i < limit - 2; i++) { - if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) { - return i; - } - } - return limit; - } - - } - - /** - * Parses a SEI data from H.264 frames and extracts samples with closed captions data. - * - * TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that - * a sample with an earlier timestamp won't be added to it. - */ - private class SeiReader extends SampleQueue implements Comparator { - - // SEI data, used for Closed Captions. - private static final int NAL_UNIT_TYPE_SEI = 6; - - private final ParsableByteArray seiBuffer; - private final TreeSet internalQueue; - - public SeiReader(SamplePool samplePool) { - super(samplePool); - setMediaFormat(MediaFormat.createEia608Format()); - seiBuffer = new ParsableByteArray(); - internalQueue = new TreeSet(this); - } - - @SuppressLint("InlinedApi") - public void read(byte[] data, int length, long pesTimeUs) { - seiBuffer.reset(data, length); - while (seiBuffer.bytesLeft() > 0) { - int currentOffset = seiBuffer.getPosition(); - int seiOffset = Mp4Util.findNalUnit(data, currentOffset, length, NAL_UNIT_TYPE_SEI); - if (seiOffset == length) { - return; - } - seiBuffer.skip(seiOffset + 4 - currentOffset); - int ccDataSize = Eia608Parser.parseHeader(seiBuffer); - if (ccDataSize > 0) { - addSample(Sample.TYPE_MISC, seiBuffer, ccDataSize, pesTimeUs, true); - } - } - } - - @Override - public int compare(Sample first, Sample second) { - // Note - We don't expect samples to have identical timestamps. - return first.timeUs <= second.timeUs ? -1 : 1; - } - - @Override - protected synchronized Sample internalPeekSample() { - return internalQueue.isEmpty() ? null : internalQueue.first(); - } - - @Override - protected synchronized Sample internalPollSample() { - return internalQueue.pollFirst(); - } - - @Override - protected synchronized void internalQueueSample(Sample sample) { - internalQueue.add(sample); - } - - } - - /** - * Parses a continuous ADTS byte stream and extracts individual frames. - */ - private class AdtsReader extends PesPayloadReader { - - private static final int STATE_FINDING_SYNC = 0; - private static final int STATE_READING_HEADER = 1; - private static final int STATE_READING_SAMPLE = 2; - - private static final int HEADER_SIZE = 5; - private static final int CRC_SIZE = 2; - - private final ParsableBitArray adtsScratch; - - private int state; - private int bytesRead; - - // Used to find the header. - private boolean lastByteWasOxFF; - private boolean hasCrc; - - // Parsed from the header. - private long frameDurationUs; - private int sampleSize; - - // Used when reading the samples. - private long timeUs; - private Sample currentSample; - - public AdtsReader(SamplePool samplePool) { - super(samplePool); - adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); - state = STATE_FINDING_SYNC; - } - - @Override - public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { - if (startOfPacket) { - timeUs = pesTimeUs; - } - while (data.bytesLeft() > 0) { - switch (state) { - case STATE_FINDING_SYNC: - if (skipToNextSync(data)) { - bytesRead = 0; - state = STATE_READING_HEADER; - } - break; - case STATE_READING_HEADER: - int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; - if (continueRead(data, adtsScratch.getData(), targetLength)) { - parseHeader(); - currentSample = getSample(Sample.TYPE_AUDIO); - currentSample.timeUs = timeUs; - currentSample.isKeyframe = true; - bytesRead = 0; - state = STATE_READING_SAMPLE; - } - break; - case STATE_READING_SAMPLE: - int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); - addToSample(currentSample, data, bytesToRead); - bytesRead += bytesToRead; - if (bytesRead == sampleSize) { - addSample(currentSample); - currentSample = null; - timeUs += frameDurationUs; - bytesRead = 0; - state = STATE_FINDING_SYNC; - } - break; - } - } - } - - @Override - public void packetFinished() { - // Do nothing. - } - - @Override - public void release() { - super.release(); - if (currentSample != null) { - recycle(currentSample); - currentSample = null; - } - } - - /** - * Continues a read from the provided {@code source} into a given {@code target}. It's assumed - * that the data should be written into {@code target} starting from an offset of zero. - * - * @param source The source from which to read. - * @param target The target into which data is to be read. - * @param targetLength The target length of the read. - * @return Whether the target length was reached. - */ - private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { - int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); - source.readBytes(target, bytesRead, bytesToRead); - bytesRead += bytesToRead; - return bytesRead == targetLength; - } - - /** - * Locates the next sync word, advancing the position to the byte that immediately follows it. - * If a sync word was not located, the position is advanced to the limit. - * - * @param pesBuffer The buffer whose position should be advanced. - * @return True if a sync word position was found. False otherwise. - */ - private boolean skipToNextSync(ParsableByteArray pesBuffer) { - byte[] adtsData = pesBuffer.data; - int startOffset = pesBuffer.getPosition(); - int endOffset = pesBuffer.length(); - for (int i = startOffset; i < endOffset; i++) { - boolean byteIsOxFF = (adtsData[i] & 0xFF) == 0xFF; - boolean found = lastByteWasOxFF && !byteIsOxFF && (adtsData[i] & 0xF0) == 0xF0; - lastByteWasOxFF = byteIsOxFF; - if (found) { - hasCrc = (adtsData[i] & 0x1) == 0; - pesBuffer.setPosition(i + 1); - return true; - } - } - pesBuffer.setPosition(endOffset); - return false; - } - - /** - * Parses the sample header. - */ - private void parseHeader() { - adtsScratch.setPosition(0); - - if (!hasMediaFormat()) { - int audioObjectType = adtsScratch.readBits(2) + 1; - int sampleRateIndex = adtsScratch.readBits(4); - adtsScratch.skipBits(1); - int channelConfig = adtsScratch.readBits(3); - - byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAudioSpecificConfig( - audioObjectType, sampleRateIndex, channelConfig); - Pair audioParams = CodecSpecificDataUtil.parseAudioSpecificConfig( - audioSpecificConfig); - - MediaFormat mediaFormat = MediaFormat.createAudioFormat(MimeTypes.AUDIO_AAC, - MediaFormat.NO_VALUE, audioParams.second, audioParams.first, - Collections.singletonList(audioSpecificConfig)); - frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate; - setMediaFormat(mediaFormat); - } else { - adtsScratch.skipBits(10); - } - - adtsScratch.skipBits(4); - sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE; - if (hasCrc) { - sampleSize -= CRC_SIZE; - } - } - - } - - /** - * Parses ID3 data and extracts individual text information frames. - */ - private class Id3Reader extends PesPayloadReader { - - private Sample currentSample; - - public Id3Reader(SamplePool samplePool) { - super(samplePool); - setMediaFormat(MediaFormat.createId3Format()); - } - - @SuppressLint("InlinedApi") - @Override - public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { - if (startOfPacket) { - currentSample = getSample(Sample.TYPE_MISC); - currentSample.timeUs = pesTimeUs; - currentSample.isKeyframe = true; - } - if (currentSample != null) { - addToSample(currentSample, data, data.bytesLeft()); - } - } - - @Override - public void packetFinished() { - addSample(currentSample); - currentSample = null; - } - - @Override - public void release() { - super.release(); - if (currentSample != null) { - recycle(currentSample); - currentSample = null; - } - } - - } - - /** - * 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 static 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; - } - - } - - /** - * An internal variant of {@link SampleHolder} for internal pooling and buffering. - */ - private static 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/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java new file mode 100644 index 0000000000..7bea5b17c1 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java @@ -0,0 +1,192 @@ +/* + * 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.C; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.util.CodecSpecificDataUtil; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; + +import android.util.Pair; + +import java.util.Collections; + +/** + * Parses a continuous ADTS byte stream and extracts individual frames. + */ +/* package */ class AdtsReader extends PesPayloadReader { + + private static final int STATE_FINDING_SYNC = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_SAMPLE = 2; + + private static final int HEADER_SIZE = 5; + private static final int CRC_SIZE = 2; + + private final ParsableBitArray adtsScratch; + + private int state; + private int bytesRead; + + // Used to find the header. + private boolean lastByteWasOxFF; + private boolean hasCrc; + + // Parsed from the header. + private long frameDurationUs; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + private Sample currentSample; + + public AdtsReader(SamplePool samplePool) { + super(samplePool); + adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); + state = STATE_FINDING_SYNC; + } + + @Override + public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { + if (startOfPacket) { + timeUs = pesTimeUs; + } + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC: + if (skipToNextSync(data)) { + bytesRead = 0; + state = STATE_READING_HEADER; + } + break; + case STATE_READING_HEADER: + int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; + if (continueRead(data, adtsScratch.getData(), targetLength)) { + parseHeader(); + currentSample = getSample(Sample.TYPE_AUDIO); + currentSample.timeUs = timeUs; + currentSample.isKeyframe = true; + bytesRead = 0; + state = STATE_READING_SAMPLE; + } + break; + case STATE_READING_SAMPLE: + int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + addToSample(currentSample, data, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + addSample(currentSample); + currentSample = null; + timeUs += frameDurationUs; + bytesRead = 0; + state = STATE_FINDING_SYNC; + } + break; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + @Override + public void release() { + super.release(); + if (currentSample != null) { + recycle(currentSample); + currentSample = null; + } + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read. + * @param targetLength The target length of the read. + * @return Whether the target length was reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + source.readBytes(target, bytesRead, bytesToRead); + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + /** + * Locates the next sync word, advancing the position to the byte that immediately follows it. + * If a sync word was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + * @return True if a sync word position was found. False otherwise. + */ + private boolean skipToNextSync(ParsableByteArray pesBuffer) { + byte[] adtsData = pesBuffer.data; + int startOffset = pesBuffer.getPosition(); + int endOffset = pesBuffer.length(); + for (int i = startOffset; i < endOffset; i++) { + boolean byteIsOxFF = (adtsData[i] & 0xFF) == 0xFF; + boolean found = lastByteWasOxFF && !byteIsOxFF && (adtsData[i] & 0xF0) == 0xF0; + lastByteWasOxFF = byteIsOxFF; + if (found) { + hasCrc = (adtsData[i] & 0x1) == 0; + pesBuffer.setPosition(i + 1); + return true; + } + } + pesBuffer.setPosition(endOffset); + return false; + } + + /** + * Parses the sample header. + */ + private void parseHeader() { + adtsScratch.setPosition(0); + + if (!hasMediaFormat()) { + int audioObjectType = adtsScratch.readBits(2) + 1; + int sampleRateIndex = adtsScratch.readBits(4); + adtsScratch.skipBits(1); + int channelConfig = adtsScratch.readBits(3); + + byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAudioSpecificConfig( + audioObjectType, sampleRateIndex, channelConfig); + Pair audioParams = CodecSpecificDataUtil.parseAudioSpecificConfig( + audioSpecificConfig); + + MediaFormat mediaFormat = MediaFormat.createAudioFormat(MimeTypes.AUDIO_AAC, + MediaFormat.NO_VALUE, audioParams.second, audioParams.first, + Collections.singletonList(audioSpecificConfig)); + frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate; + setMediaFormat(mediaFormat); + } else { + adtsScratch.skipBits(10); + } + + adtsScratch.skipBits(4); + sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE; + if (hasCrc) { + sampleSize -= CRC_SIZE; + } + } + +} 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 new file mode 100644 index 0000000000..6bf1d5c4f5 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java @@ -0,0 +1,266 @@ +/* + * 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.MediaFormat; +import com.google.android.exoplayer.mp4.Mp4Util; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parses a continuous H264 byte stream and extracts individual frames. + */ +/* package */ class H264Reader extends PesPayloadReader { + + private static final int NAL_UNIT_TYPE_IDR = 5; + private static final int NAL_UNIT_TYPE_SPS = 7; + private static final int NAL_UNIT_TYPE_PPS = 8; + private static final int NAL_UNIT_TYPE_AUD = 9; + + private final SeiReader seiReader; + + private Sample currentSample; + + public H264Reader(SamplePool samplePool, SeiReader seiReader) { + super(samplePool); + this.seiReader = seiReader; + } + + @Override + public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { + while (data.bytesLeft() > 0) { + if (readToNextAudUnit(data, pesTimeUs)) { + currentSample.isKeyframe = currentSample.size + > Mp4Util.findNalUnit(currentSample.data, 0, currentSample.size, NAL_UNIT_TYPE_IDR); + if (!hasMediaFormat() && currentSample.isKeyframe) { + parseMediaFormat(currentSample); + } + seiReader.read(currentSample.data, currentSample.size, currentSample.timeUs); + addSample(currentSample); + currentSample = null; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + @Override + public void release() { + super.release(); + if (currentSample != null) { + recycle(currentSample); + currentSample = null; + } + } + + /** + * Reads data up to (but not including) the start of the next AUD unit. + * + * @param data The data to consume. + * @param pesTimeUs The corresponding time. + * @return True if the current sample is now complete. False otherwise. + */ + private boolean readToNextAudUnit(ParsableByteArray data, long pesTimeUs) { + int pesOffset = data.getPosition(); + int pesLimit = data.length(); + + // TODO: We probably need to handle the case where the AUD start code was split across the + // previous and current data buffers. + int audOffset = Mp4Util.findNalUnit(data.data, pesOffset, pesLimit, NAL_UNIT_TYPE_AUD); + int bytesToNextAud = audOffset - pesOffset; + if (bytesToNextAud == 0) { + if (currentSample == null) { + currentSample = getSample(Sample.TYPE_VIDEO); + currentSample.timeUs = pesTimeUs; + addToSample(currentSample, data, 4); + return false; + } else { + return true; + } + } else if (currentSample != null) { + addToSample(currentSample, data, bytesToNextAud); + return data.bytesLeft() > 0; + } else { + data.skip(bytesToNextAud); + return false; + } + } + + private void parseMediaFormat(Sample sample) { + byte[] sampleData = sample.data; + int sampleSize = sample.size; + // Locate the SPS and PPS units. + int spsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_SPS); + int ppsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_PPS); + if (spsOffset == sampleSize || ppsOffset == sampleSize) { + return; + } + // Determine the length of the units, and copy them to build the initialization data. + int spsLength = Mp4Util.findNalUnit(sampleData, spsOffset + 3, sampleSize) - spsOffset; + int ppsLength = Mp4Util.findNalUnit(sampleData, ppsOffset + 3, sampleSize) - ppsOffset; + byte[] spsData = new byte[spsLength]; + byte[] ppsData = new byte[ppsLength]; + System.arraycopy(sampleData, spsOffset, spsData, 0, spsLength); + System.arraycopy(sampleData, ppsOffset, ppsData, 0, ppsLength); + List initializationData = new ArrayList(); + initializationData.add(spsData); + initializationData.add(ppsData); + + // Unescape and then parse the SPS unit. + byte[] unescapedSps = unescapeStream(spsData, 0, spsLength); + ParsableBitArray bitArray = new ParsableBitArray(unescapedSps); + bitArray.skipBits(32); // NAL header + int profileIdc = bitArray.readBits(8); + bitArray.skipBits(16); // constraint bits (6), reserved (2) and level_idc (8) + bitArray.readUnsignedExpGolombCodedInt(); // seq_parameter_set_id + + int chromaFormatIdc = 1; // Default is 4:2:0 + if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244 + || profileIdc == 44 || profileIdc == 83 || profileIdc == 86 || profileIdc == 118 + || profileIdc == 128 || profileIdc == 138) { + chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); + if (chromaFormatIdc == 3) { + bitArray.skipBits(1); // separate_colour_plane_flag + } + bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8 + bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8 + bitArray.skipBits(1); // qpprime_y_zero_transform_bypass_flag + boolean seqScalingMatrixPresentFlag = bitArray.readBit(); + if (seqScalingMatrixPresentFlag) { + int limit = (chromaFormatIdc != 3) ? 8 : 12; + for (int i = 0; i < limit; i++) { + boolean seqScalingListPresentFlag = bitArray.readBit(); + if (seqScalingListPresentFlag) { + skipScalingList(bitArray, i < 6 ? 16 : 64); + } + } + } + } + + bitArray.readUnsignedExpGolombCodedInt(); // log2_max_frame_num_minus4 + long picOrderCntType = bitArray.readUnsignedExpGolombCodedInt(); + if (picOrderCntType == 0) { + bitArray.readUnsignedExpGolombCodedInt(); // log2_max_pic_order_cnt_lsb_minus4 + } else if (picOrderCntType == 1) { + bitArray.skipBits(1); // delta_pic_order_always_zero_flag + bitArray.readSignedExpGolombCodedInt(); // offset_for_non_ref_pic + bitArray.readSignedExpGolombCodedInt(); // offset_for_top_to_bottom_field + long numRefFramesInPicOrderCntCycle = bitArray.readUnsignedExpGolombCodedInt(); + for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) { + bitArray.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i] + } + } + bitArray.readUnsignedExpGolombCodedInt(); // max_num_ref_frames + bitArray.skipBits(1); // gaps_in_frame_num_value_allowed_flag + + int picWidthInMbs = bitArray.readUnsignedExpGolombCodedInt() + 1; + int picHeightInMapUnits = bitArray.readUnsignedExpGolombCodedInt() + 1; + boolean frameMbsOnlyFlag = bitArray.readBit(); + int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits; + if (!frameMbsOnlyFlag) { + bitArray.skipBits(1); // mb_adaptive_frame_field_flag + } + + bitArray.skipBits(1); // direct_8x8_inference_flag + int frameWidth = picWidthInMbs * 16; + int frameHeight = frameHeightInMbs * 16; + boolean frameCroppingFlag = bitArray.readBit(); + if (frameCroppingFlag) { + int frameCropLeftOffset = bitArray.readUnsignedExpGolombCodedInt(); + int frameCropRightOffset = bitArray.readUnsignedExpGolombCodedInt(); + int frameCropTopOffset = bitArray.readUnsignedExpGolombCodedInt(); + int frameCropBottomOffset = bitArray.readUnsignedExpGolombCodedInt(); + int cropUnitX, cropUnitY; + if (chromaFormatIdc == 0) { + cropUnitX = 1; + cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0); + } else { + int subWidthC = (chromaFormatIdc == 3) ? 1 : 2; + int subHeightC = (chromaFormatIdc == 1) ? 2 : 1; + cropUnitX = subWidthC; + cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0)); + } + frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX; + frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY; + } + + // Set the format. + setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, + frameWidth, frameHeight, initializationData)); + } + + private void skipScalingList(ParsableBitArray bitArray, int size) { + int lastScale = 8; + int nextScale = 8; + for (int i = 0; i < size; i++) { + if (nextScale != 0) { + int deltaScale = bitArray.readSignedExpGolombCodedInt(); + nextScale = (lastScale + deltaScale + 256) % 256; + } + lastScale = (nextScale == 0) ? lastScale : nextScale; + } + } + + /** + * Replaces occurrences of [0, 0, 3] with [0, 0]. + *

+ * See ISO/IEC 14496-10:2005(E) page 36 for more information. + */ + private byte[] unescapeStream(byte[] data, int offset, int limit) { + int position = offset; + List escapePositions = new ArrayList(); + while (position < limit) { + position = findNextUnescapeIndex(data, position, limit); + if (position < limit) { + escapePositions.add(position); + position += 3; + } + } + + int escapeCount = escapePositions.size(); + int escapedPosition = offset; // The position being read from. + int unescapedPosition = 0; // The position being written to. + byte[] unescapedData = new byte[limit - offset - escapeCount]; + for (int i = 0; i < escapeCount; i++) { + int nextEscapePosition = escapePositions.get(i); + int copyLength = nextEscapePosition - escapedPosition; + System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, copyLength); + escapedPosition += copyLength + 3; + unescapedPosition += copyLength + 2; + } + + int remainingLength = unescapedData.length - unescapedPosition; + System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, remainingLength); + return unescapedData; + } + + private int findNextUnescapeIndex(byte[] bytes, int offset, int limit) { + for (int i = offset; i < limit - 2; i++) { + if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) { + return i; + } + } + return limit; + } + +} 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 new file mode 100644 index 0000000000..b30aead7d4 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java @@ -0,0 +1,63 @@ +/* + * 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.MediaFormat; +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 { + + private Sample currentSample; + + public Id3Reader(SamplePool samplePool) { + super(samplePool); + setMediaFormat(MediaFormat.createId3Format()); + } + + @SuppressLint("InlinedApi") + @Override + public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { + if (startOfPacket) { + currentSample = getSample(Sample.TYPE_MISC); + currentSample.timeUs = pesTimeUs; + currentSample.isKeyframe = true; + } + if (currentSample != null) { + addToSample(currentSample, data, data.bytesLeft()); + } + } + + @Override + public void packetFinished() { + addSample(currentSample); + currentSample = null; + } + + @Override + public void release() { + super.release(); + if (currentSample != null) { + recycle(currentSample); + currentSample = null; + } + } + +} 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 new file mode 100644 index 0000000000..b6057e7717 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java @@ -0,0 +1,66 @@ +/* + * 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.util.ParsableByteArray; + +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Extracts individual samples from continuous byte stream, preserving original order. + */ +/* package */ abstract class PesPayloadReader extends SampleQueue { + + private final ConcurrentLinkedQueue internalQueue; + + protected PesPayloadReader(SamplePool samplePool) { + super(samplePool); + internalQueue = new ConcurrentLinkedQueue(); + } + + @Override + protected final Sample internalPeekSample() { + return internalQueue.peek(); + } + + @Override + protected final Sample internalPollSample() { + return internalQueue.poll(); + } + + @Override + protected final void internalQueueSample(Sample sample) { + internalQueue.add(sample); + } + + /** + * Consumes (possibly partial) payload data. + * + * @param data The payload data to consume. + * @param pesTimeUs The timestamp associated with the payload. + * @param startOfPacket True if this is the first time this method is being called for the + * current packet. False otherwise. + */ + public abstract void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket); + + /** + * Invoked once all of the payload data for a packet has been passed to + * {@link #consume(ParsableByteArray, long, boolean)}. The next call to + * {@link #consume(ParsableByteArray, long, boolean)} will have {@code startOfPacket == true}. + */ + public abstract void packetFinished(); + +} 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 new file mode 100644 index 0000000000..d3b145d1c8 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/Sample.java @@ -0,0 +1,55 @@ +/* + * 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 new file mode 100644 index 0000000000..240c8508cc --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/SamplePool.java @@ -0,0 +1,58 @@ +/* + * 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 new file mode 100644 index 0000000000..7b7916e683 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java @@ -0,0 +1,221 @@ +/* + * 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.MediaFormat; +import com.google.android.exoplayer.util.ParsableByteArray; + +/* package */ abstract class SampleQueue { + + private final SamplePool samplePool; + + // Accessed only by the consuming thread. + private boolean needKeyframe; + private long lastReadTimeUs; + private long spliceOutTimeUs; + + // Accessed by both the loading and consuming threads. + private volatile MediaFormat mediaFormat; + private volatile long largestParsedTimestampUs; + + protected SampleQueue(SamplePool samplePool) { + this.samplePool = samplePool; + needKeyframe = true; + lastReadTimeUs = Long.MIN_VALUE; + spliceOutTimeUs = Long.MIN_VALUE; + largestParsedTimestampUs = Long.MIN_VALUE; + } + + public long getLargestParsedTimestampUs() { + return largestParsedTimestampUs; + } + + public boolean hasMediaFormat() { + return mediaFormat != null; + } + + public MediaFormat getMediaFormat() { + return mediaFormat; + } + + protected void setMediaFormat(MediaFormat mediaFormat) { + this.mediaFormat = mediaFormat; + } + + /** + * Removes and returns the next sample from the queue. + *

+ * The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples + * queued prior to the first keyframe are discarded. + * + * @return The next sample from the queue, or null if a sample isn't available. + */ + public Sample poll() { + Sample head = peek(); + if (head != null) { + internalPollSample(); + needKeyframe = false; + lastReadTimeUs = head.timeUs; + } + return head; + } + + /** + * Like {@link #poll()}, except the returned sample is not removed from the queue. + * + * @return The next sample from the queue, or null if a sample isn't available. + */ + public Sample peek() { + Sample head = internalPeekSample(); + if (needKeyframe) { + // Peeking discard of samples until we find a keyframe or run out of available samples. + while (head != null && !head.isKeyframe) { + recycle(head); + internalPollSample(); + head = internalPeekSample(); + } + } + 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. + recycle(head); + internalPollSample(); + 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) { + recycle(head); + internalPollSample(); + head = internalPeekSample(); + // We're discarding at least one sample, so any subsequent read will need to start at + // a keyframe. + needKeyframe = true; + } + lastReadTimeUs = Long.MIN_VALUE; + } + + /** + * Clears the queue. + */ + public void release() { + Sample toRecycle = internalPollSample(); + while (toRecycle != null) { + recycle(toRecycle); + toRecycle = internalPollSample(); + } + } + + /** + * Recycles a sample. + * + * @param sample The sample to recycle. + */ + public void recycle(Sample sample) { + samplePool.recycle(sample); + } + + /** + * Attempts to configure a splice from this queue to the next. + * + * @param nextQueue The queue being spliced to. + * @return Whether the splice was configured successfully. + */ + public boolean configureSpliceTo(SampleQueue nextQueue) { + if (spliceOutTimeUs != Long.MIN_VALUE) { + // We've already configured the splice. + return true; + } + long firstPossibleSpliceTime; + Sample nextSample = internalPeekSample(); + if (nextSample != null) { + firstPossibleSpliceTime = nextSample.timeUs; + } else { + firstPossibleSpliceTime = lastReadTimeUs + 1; + } + Sample nextQueueSample = nextQueue.internalPeekSample(); + while (nextQueueSample != null + && (nextQueueSample.timeUs < firstPossibleSpliceTime || !nextQueueSample.isKeyframe)) { + // Discard samples from the next queue for as long as they are before the earliest possible + // splice time, or not keyframes. + nextQueue.internalPollSample(); + nextQueueSample = nextQueue.internalPeekSample(); + } + if (nextQueueSample != null) { + // We've found a keyframe in the next queue that can serve as the splice point. Set the + // splice point now. + spliceOutTimeUs = nextQueueSample.timeUs; + return true; + } + return false; + } + + /** + * Obtains a Sample object to use. + * + * @param type The type of the sample. + * @return The sample. + */ + protected Sample getSample(int type) { + return samplePool.get(type); + } + + /** + * Creates a new Sample and adds it to the queue. + * + * @param type The type of the sample. + * @param buffer The buffer to read sample data. + * @param sampleSize The size of the sample data. + * @param sampleTimeUs The sample time stamp. + * @param isKeyframe True if the sample is a keyframe. False otherwise. + */ + protected void addSample(int type, ParsableByteArray buffer, int sampleSize, long sampleTimeUs, + boolean isKeyframe) { + Sample sample = getSample(type); + addToSample(sample, buffer, sampleSize); + sample.isKeyframe = isKeyframe; + sample.timeUs = sampleTimeUs; + addSample(sample); + } + + protected void addSample(Sample sample) { + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); + internalQueueSample(sample); + } + + protected void addToSample(Sample sample, ParsableByteArray buffer, int size) { + if (sample.data.length - sample.size < size) { + sample.expand(size - sample.data.length + sample.size); + } + buffer.readBytes(sample.data, sample.size, size); + sample.size += size; + } + + protected abstract Sample internalPeekSample(); + protected abstract Sample internalPollSample(); + protected abstract void internalQueueSample(Sample sample); + +} 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 new file mode 100644 index 0000000000..0d12d60de3 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java @@ -0,0 +1,87 @@ +/* + * 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.MediaFormat; +import com.google.android.exoplayer.mp4.Mp4Util; +import com.google.android.exoplayer.text.eia608.Eia608Parser; +import com.google.android.exoplayer.util.ParsableByteArray; + +import android.annotation.SuppressLint; + +import java.util.Comparator; +import java.util.TreeSet; + +/** + * Parses a SEI data from H.264 frames and extracts samples with closed captions data. + * + * TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that + * a sample with an earlier timestamp won't be added to it. + */ +/* package */ class SeiReader extends SampleQueue implements Comparator { + + // SEI data, used for Closed Captions. + private static final int NAL_UNIT_TYPE_SEI = 6; + + private final ParsableByteArray seiBuffer; + private final TreeSet internalQueue; + + public SeiReader(SamplePool samplePool) { + super(samplePool); + setMediaFormat(MediaFormat.createEia608Format()); + seiBuffer = new ParsableByteArray(); + internalQueue = new TreeSet(this); + } + + @SuppressLint("InlinedApi") + public void read(byte[] data, int length, long pesTimeUs) { + seiBuffer.reset(data, length); + while (seiBuffer.bytesLeft() > 0) { + int currentOffset = seiBuffer.getPosition(); + int seiOffset = Mp4Util.findNalUnit(data, currentOffset, length, NAL_UNIT_TYPE_SEI); + if (seiOffset == length) { + return; + } + seiBuffer.skip(seiOffset + 4 - currentOffset); + int ccDataSize = Eia608Parser.parseHeader(seiBuffer); + if (ccDataSize > 0) { + addSample(Sample.TYPE_MISC, seiBuffer, ccDataSize, pesTimeUs, true); + } + } + } + + @Override + public int compare(Sample first, Sample second) { + // Note - We don't expect samples to have identical timestamps. + return first.timeUs <= second.timeUs ? -1 : 1; + } + + @Override + protected synchronized Sample internalPeekSample() { + return internalQueue.isEmpty() ? null : internalQueue.first(); + } + + @Override + protected synchronized Sample internalPollSample() { + return internalQueue.pollFirst(); + } + + @Override + protected synchronized void internalQueueSample(Sample sample) { + internalQueue.add(sample); + } + +} 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 new file mode 100644 index 0000000000..5f698fd6a5 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java @@ -0,0 +1,629 @@ +/* + * 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.C; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; + +import android.annotation.SuppressLint; +import android.media.MediaExtractor; +import android.util.Log; +import android.util.SparseArray; + +import java.io.IOException; + +/** + * Facilitates the extraction of data from the MPEG-2 TS container format. + */ +public final class TsExtractor { + + private static final String TAG = "TsExtractor"; + + private static final int TS_PACKET_SIZE = 188; + private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. + private static final int TS_PAT_PID = 0; + + private static final int TS_STREAM_TYPE_AAC = 0x0F; + private static final int TS_STREAM_TYPE_H264 = 0x1B; + private static final int TS_STREAM_TYPE_ID3 = 0x15; + private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 + + private static final long MAX_PTS = 0x1FFFFFFFFL; + + private final ParsableByteArray tsPacketBuffer; + private final SparseArray sampleQueues; // Indexed by streamType + private final SparseArray tsPayloadReaders; // Indexed by pid + private final SamplePool samplePool; + private final boolean shouldSpliceIn; + private final long firstSampleTimestamp; + private final ParsableBitArray tsScratch; + + // Accessed only by the consuming thread. + private boolean spliceConfigured; + + // Accessed only by the loading thread. + private int tsPacketBytesRead; + private long timestampOffsetUs; + private long lastPts; + + // Accessed by both the loading and consuming threads. + private volatile boolean prepared; + + public TsExtractor(long firstSampleTimestamp, SamplePool samplePool, boolean shouldSpliceIn) { + this.firstSampleTimestamp = firstSampleTimestamp; + this.samplePool = samplePool; + this.shouldSpliceIn = shouldSpliceIn; + tsScratch = new ParsableBitArray(new byte[3]); + tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); + sampleQueues = new SparseArray(); + tsPayloadReaders = new SparseArray(); + tsPayloadReaders.put(TS_PAT_PID, new PatReader()); + lastPts = Long.MIN_VALUE; + } + + /** + * Gets the number of available tracks. + *

+ * This method should only be called after the extractor has been prepared. + * + * @return The number of available tracks. + */ + public int getTrackCount() { + Assertions.checkState(prepared); + return sampleQueues.size(); + } + + /** + * Gets the format of the specified track. + *

+ * This method must only be called after the extractor has been prepared. + * + * @param track The track index. + * @return The corresponding format. + */ + public MediaFormat getFormat(int track) { + Assertions.checkState(prepared); + return sampleQueues.valueAt(track).getMediaFormat(); + } + + /** + * Whether the extractor is prepared. + * + * @return True if the extractor is prepared. False otherwise. + */ + public boolean isPrepared() { + return prepared; + } + + /** + * Releases the extractor, recycling any pending or incomplete samples to the sample pool. + *

+ * This method should not be called whilst {@link #read(DataSource)} is also being invoked. + */ + public void release() { + for (int i = 0; i < sampleQueues.size(); i++) { + sampleQueues.valueAt(i).release(); + } + } + + /** + * Attempts to configure a splice from this extractor to the next. + *

+ * The splice is performed such that for each track the samples read from the next extractor + * start with a keyframe, and continue from where the samples read from this extractor finish. + * A successful splice may discard samples from either or both extractors. + *

+ * Splice configuration may fail if the next extractor is not yet in a state that allows the + * splice to be performed. Calling this method is a noop if the splice has already been + * configured. Hence this method should be called repeatedly during the window within which a + * splice can be performed. + * + * @param nextExtractor The extractor being spliced to. + */ + public void configureSpliceTo(TsExtractor nextExtractor) { + Assertions.checkState(prepared); + if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) { + // The splice is already configured, or the next extractor doesn't want to be spliced in, or + // the next extractor isn't ready to be spliced in. + return; + } + boolean spliceConfigured = true; + for (int i = 0; i < sampleQueues.size(); i++) { + spliceConfigured &= sampleQueues.valueAt(i).configureSpliceTo( + nextExtractor.sampleQueues.valueAt(i)); + } + this.spliceConfigured = spliceConfigured; + return; + } + + /** + * Gets the largest timestamp of any sample parsed by the extractor. + * + * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed. + */ + public long getLargestSampleTimestamp() { + long largestParsedTimestampUs = Long.MIN_VALUE; + for (int i = 0; i < sampleQueues.size(); i++) { + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, + sampleQueues.valueAt(i).getLargestParsedTimestampUs()); + } + return largestParsedTimestampUs; + } + + /** + * Gets the next sample for the specified track. + * + * @param track The track from which to read. + * @param out A {@link SampleHolder} into which the next sample should be read. + * @return True if a sample was read. False otherwise. + */ + public boolean getSample(int track, SampleHolder out) { + Assertions.checkState(prepared); + SampleQueue sampleQueue = sampleQueues.valueAt(track); + Sample sample = sampleQueue.poll(); + if (sample == null) { + return false; + } + convert(sample, out); + sampleQueue.recycle(sample); + return true; + } + + /** + * Discards samples for the specified track up to the specified time. + * + * @param track The track from which samples should be discarded. + * @param timeUs The time up to which samples should be discarded, in microseconds. + */ + public void discardUntil(int track, long timeUs) { + Assertions.checkState(prepared); + sampleQueues.valueAt(track).discardUntil(timeUs); + } + + /** + * Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the + * specified track. + * + * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)} + * for the specified track. False otherwise. + */ + public boolean hasSamples(int track) { + Assertions.checkState(prepared); + return sampleQueues.valueAt(track).peek() != null; + } + + private boolean checkPrepared() { + int pesPayloadReaderCount = sampleQueues.size(); + if (pesPayloadReaderCount == 0) { + return false; + } + for (int i = 0; i < pesPayloadReaderCount; i++) { + if (!sampleQueues.valueAt(i).hasMediaFormat()) { + return false; + } + } + return true; + } + + /** + * Reads up to a single TS packet. + * + * @param dataSource The {@link DataSource} from which to read. + * @throws IOException If an error occurred reading from the source. + * @return The number of bytes read from the source. + */ + public int read(DataSource dataSource) throws IOException { + int bytesRead = dataSource.read(tsPacketBuffer.data, tsPacketBytesRead, + TS_PACKET_SIZE - tsPacketBytesRead); + if (bytesRead == -1) { + return -1; + } + + tsPacketBytesRead += bytesRead; + if (tsPacketBytesRead < TS_PACKET_SIZE) { + // We haven't read the whole packet yet. + return bytesRead; + } + + // Reset before reading the packet. + tsPacketBytesRead = 0; + tsPacketBuffer.setPosition(0); + tsPacketBuffer.setLimit(TS_PACKET_SIZE); + + int syncByte = tsPacketBuffer.readUnsignedByte(); + if (syncByte != TS_SYNC_BYTE) { + return bytesRead; + } + + tsPacketBuffer.readBytes(tsScratch, 3); + tsScratch.skipBits(1); // transport_error_indicator + boolean payloadUnitStartIndicator = tsScratch.readBit(); + tsScratch.skipBits(1); // transport_priority + int pid = tsScratch.readBits(13); + tsScratch.skipBits(2); // transport_scrambling_control + boolean adaptationFieldExists = tsScratch.readBit(); + boolean payloadExists = tsScratch.readBit(); + // Last 4 bits of scratch are skipped: continuity_counter + + // Skip the adaptation field. + if (adaptationFieldExists) { + int adaptationFieldLength = tsPacketBuffer.readUnsignedByte(); + tsPacketBuffer.skip(adaptationFieldLength); + } + + // Read the payload. + if (payloadExists) { + TsPayloadReader payloadReader = tsPayloadReaders.get(pid); + if (payloadReader != null) { + payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); + } + } + + if (!prepared) { + prepared = checkPrepared(); + } + + return bytesRead; + } + + @SuppressLint("InlinedApi") + private void convert(Sample in, SampleHolder out) { + if (out.data == null || out.data.capacity() < in.size) { + out.replaceBuffer(in.size); + } + if (out.data != null) { + out.data.put(in.data, 0, in.size); + } + out.size = in.size; + out.flags = in.isKeyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; + out.timeUs = in.timeUs; + } + + /** + * Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound. + * + * @param pts The raw PTS value. + * @return The corresponding time in microseconds. + */ + /* package */ long ptsToTimeUs(long pts) { + if (lastPts != Long.MIN_VALUE) { + // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), + // and we need to snap to the one closest to lastPts. + long closestWrapCount = (lastPts + (MAX_PTS / 2)) / MAX_PTS; + long ptsWrapBelow = pts + (MAX_PTS * (closestWrapCount - 1)); + long ptsWrapAbove = pts + (MAX_PTS * closestWrapCount); + pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts) + ? ptsWrapBelow : ptsWrapAbove; + } + // Calculate the corresponding timestamp. + long timeUs = (pts * C.MICROS_PER_SECOND) / 90000; + // If we haven't done the initial timestamp adjustment, do it now. + if (lastPts == Long.MIN_VALUE) { + timestampOffsetUs = firstSampleTimestamp - timeUs; + } + // Record the adjusted PTS to adjust for wraparound next time. + lastPts = pts; + return timeUs + timestampOffsetUs; + } + + /** + * Parses TS packet payload data. + */ + private abstract static class TsPayloadReader { + + public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator); + + } + + /** + * Parses Program Association Table data. + */ + private class PatReader extends TsPayloadReader { + + private final ParsableBitArray patScratch; + + public PatReader() { + patScratch = new ParsableBitArray(new byte[4]); + } + + @Override + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + // Skip pointer. + if (payloadUnitStartIndicator) { + int pointerField = data.readUnsignedByte(); + data.skip(pointerField); + } + + data.readBytes(patScratch, 3); + patScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) + int sectionLength = patScratch.readBits(12); + // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1), + // section_number (8), last_section_number (8) + data.skip(5); + + int programCount = (sectionLength - 9) / 4; + for (int i = 0; i < programCount; i++) { + data.readBytes(patScratch, 4); + patScratch.skipBits(19); // program_number (16), reserved (3) + int pid = patScratch.readBits(13); + tsPayloadReaders.put(pid, new PmtReader()); + } + + // Skip CRC_32. + } + + } + + /** + * Parses Program Map Table. + */ + private class PmtReader extends TsPayloadReader { + + private final ParsableBitArray pmtScratch; + + public PmtReader() { + pmtScratch = new ParsableBitArray(new byte[5]); + } + + @Override + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + // Skip pointer. + if (payloadUnitStartIndicator) { + int pointerField = data.readUnsignedByte(); + data.skip(pointerField); + } + + data.readBytes(pmtScratch, 3); + pmtScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) + int sectionLength = pmtScratch.readBits(12); + + // program_number (16), reserved (2), version_number (5), current_next_indicator (1), + // section_number (8), last_section_number (8), reserved (3), PCR_PID (13) + // Skip the rest of the PMT header. + data.skip(7); + + data.readBytes(pmtScratch, 2); + pmtScratch.skipBits(4); + int programInfoLength = pmtScratch.readBits(12); + + // Skip the descriptors. + data.skip(programInfoLength); + + int entriesSize = sectionLength - 9 /* Size of the rest of the fields before descriptors */ + - programInfoLength - 4 /* CRC size */; + while (entriesSize > 0) { + data.readBytes(pmtScratch, 5); + int streamType = pmtScratch.readBits(8); + pmtScratch.skipBits(3); // reserved + int elementaryPid = pmtScratch.readBits(13); + pmtScratch.skipBits(4); // reserved + int esInfoLength = pmtScratch.readBits(12); + + // Skip the descriptors. + data.skip(esInfoLength); + entriesSize -= esInfoLength + 5; + + if (sampleQueues.get(streamType) != null) { + continue; + } + + PesPayloadReader pesPayloadReader = null; + switch (streamType) { + case TS_STREAM_TYPE_AAC: + pesPayloadReader = new AdtsReader(samplePool); + break; + case TS_STREAM_TYPE_H264: + SeiReader seiReader = new SeiReader(samplePool); + sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader); + pesPayloadReader = new H264Reader(samplePool, seiReader); + break; + case TS_STREAM_TYPE_ID3: + pesPayloadReader = new Id3Reader(samplePool); + break; + } + + if (pesPayloadReader != null) { + sampleQueues.put(streamType, pesPayloadReader); + tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader)); + } + } + + // Skip CRC_32. + } + + } + + /** + * Parses PES packet data and extracts samples. + */ + private class PesReader extends TsPayloadReader { + + private static final int STATE_FINDING_HEADER = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_HEADER_EXTENSION = 2; + private static final int STATE_READING_BODY = 3; + + private static final int HEADER_SIZE = 9; + private static final int MAX_HEADER_EXTENSION_SIZE = 5; + + private final ParsableBitArray pesScratch; + private final PesPayloadReader pesPayloadReader; + + private int state; + private int bytesRead; + private boolean bodyStarted; + + private boolean ptsFlag; + private int extendedHeaderLength; + + private int payloadSize; + + private long timeUs; + + public PesReader(PesPayloadReader pesPayloadReader) { + this.pesPayloadReader = pesPayloadReader; + pesScratch = new ParsableBitArray(new byte[HEADER_SIZE]); + state = STATE_FINDING_HEADER; + } + + @Override + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + if (payloadUnitStartIndicator) { + switch (state) { + case STATE_FINDING_HEADER: + case STATE_READING_HEADER: + // Expected. + break; + case STATE_READING_HEADER_EXTENSION: + Log.w(TAG, "Unexpected start indicator reading extended header"); + break; + case STATE_READING_BODY: + // If payloadSize == -1 then the length of the previous packet was unspecified, and so + // we only know that it's finished now that we've seen the start of the next one. This + // is expected. If payloadSize != -1, then the length of the previous packet was known, + // but we didn't receive that amount of data. This is not expected. + if (payloadSize != -1) { + Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes"); + } + // Either way, if the body was started, notify the reader that it has now finished. + if (bodyStarted) { + pesPayloadReader.packetFinished(); + } + break; + } + setState(STATE_READING_HEADER); + } + + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_HEADER: + data.skip(data.bytesLeft()); + break; + case STATE_READING_HEADER: + if (continueRead(data, pesScratch.getData(), HEADER_SIZE)) { + setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER); + } + break; + case STATE_READING_HEADER_EXTENSION: + int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); + // Read as much of the extended header as we're interested in, and skip the rest. + if (continueRead(data, pesScratch.getData(), readLength) + && continueRead(data, null, extendedHeaderLength)) { + parseHeaderExtension(); + bodyStarted = false; + setState(STATE_READING_BODY); + } + break; + case STATE_READING_BODY: + readLength = data.bytesLeft(); + int padding = payloadSize == -1 ? 0 : readLength - payloadSize; + if (padding > 0) { + readLength -= padding; + data.setLimit(data.getPosition() + readLength); + } + pesPayloadReader.consume(data, timeUs, !bodyStarted); + bodyStarted = true; + if (payloadSize != -1) { + payloadSize -= readLength; + if (payloadSize == 0) { + pesPayloadReader.packetFinished(); + setState(STATE_READING_HEADER); + } + } + break; + } + } + } + + private void setState(int state) { + this.state = state; + bytesRead = 0; + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read, or {@code null} to skip. + * @param targetLength The target length of the read. + * @return Whether the target length has been reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + if (bytesToRead <= 0) { + return true; + } else if (target == null) { + source.skip(bytesToRead); + } else { + source.readBytes(target, bytesRead, bytesToRead); + } + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + private boolean parseHeader() { + pesScratch.setPosition(0); + int startCodePrefix = pesScratch.readBits(24); + if (startCodePrefix != 0x000001) { + Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); + payloadSize = -1; + return false; + } + + pesScratch.skipBits(8); // stream_id. + int packetLength = pesScratch.readBits(16); + // First 8 bits are skipped: '10' (2), PES_scrambling_control (2), PES_priority (1), + // data_alignment_indicator (1), copyright (1), original_or_copy (1) + pesScratch.skipBits(8); + ptsFlag = pesScratch.readBit(); + // DTS_flag (1), ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1), + // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1) + pesScratch.skipBits(7); + extendedHeaderLength = pesScratch.readBits(8); + + if (packetLength == 0) { + payloadSize = -1; + } else { + payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */ + - HEADER_SIZE - extendedHeaderLength; + } + return true; + } + + private void parseHeaderExtension() { + pesScratch.setPosition(0); + timeUs = 0; + if (ptsFlag) { + pesScratch.skipBits(4); // '0010' + long pts = pesScratch.readBitsLong(3) << 30; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBitsLong(15) << 15; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBitsLong(15); + pesScratch.skipBits(1); // marker_bit + timeUs = ptsToTimeUs(pts); + } + } + + } + +} From f7fb4d4c358b86a2ab031b4a5852f0ba8bcb3128 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 12 Feb 2015 12:46:58 +0000 Subject: [PATCH 05/17] Optimize NAL unit search. I'm not really a fan of micro-optimizations, but given this method scans through every H264 frame in the HLS case, it seems worthwhile. The trick here is to examine the first 7 bits of the third byte first. If they're not all 0s, then we know that we haven't found a NAL unit, and also that we wont find one at the next two positions. This allows the loop to increment 3 bytes at a time. Speedup is around 60% on Art according to some ad-hoc benchmarking. --- .../google/android/exoplayer/mp4/Mp4Util.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java index 6a7123d555..09abe2ccf1 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java @@ -102,6 +102,7 @@ public final class Mp4Util { /** * Like {@link #findNalUnit(byte[], int, int, int)} with {@code type == -1}. * + * @param data The data to search. * @param startOffset The offset (inclusive) in the data to start the search. * @param endOffset The offset (exclusive) in the data to end the search. * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. @@ -116,17 +117,28 @@ public final class Mp4Util { * For a NAL unit to be found, its first four bytes must be contained within the part of the * array being searched. * - * @param type The type of the NAL unit to search for, or -1 for any NAL unit. + * @param data The data to search. * @param startOffset The offset (inclusive) in the data to start the search. * @param endOffset The offset (exclusive) in the data to end the search. + * @param type The type of the NAL unit to search for, or -1 for any NAL unit. * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. */ public static int findNalUnit(byte[] data, int startOffset, int endOffset, int type) { - for (int i = startOffset; i < endOffset - 3; i++) { - // Check for NAL unit start code prefix == 0x000001. - if ((data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1) - && (type == -1 || (type == (data[i + 3] & 0x1F)))) { - return i; + int limit = endOffset - 2; + // We're looking for the NAL unit start code prefix 0x000001, followed by a byte that matches + // the specified type. The value of i tracks the index of the third byte in the four bytes + // being examined. + for (int i = startOffset + 2; i < limit; i += 3) { + if ((data[i] & 0xFE) != 0) { + // There isn't a NAL prefix here, or at the next two positions. Do nothing and let the + // loop advance the index by three. + } else if ((data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1) + && (type == -1 || (type == (data[i + 1] & 0x1F)))) { + return i - 2; + } else { + // There isn't a NAL prefix here, but there might be at the next position. We should + // only skip forward by one. The loop will skip forward by three, so subtract two here. + i -= 2; } } return endOffset; From 784431f3e08099a4f22a130eb2a3f2e1b054879d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 12 Feb 2015 12:48:27 +0000 Subject: [PATCH 06/17] Move EIA reordering back to the renderer (sorry for churn). Reordering in the extractor isn't going to work well with the optimizations I'm making there. This change moves sorting back to the renderer, although keeps all of the renderer simplifications. It's basically just moving where the sort happens from one place to another. --- .../hls/parser/PesPayloadReader.java | 20 ------ .../exoplayer/hls/parser/SampleQueue.java | 36 +++++------ .../exoplayer/hls/parser/SeiReader.java | 28 +-------- .../exoplayer/text/eia608/ClosedCaption.java | 18 +----- .../text/eia608/ClosedCaptionCtrl.java | 4 +- .../text/eia608/ClosedCaptionList.java | 39 ++++++++++++ .../text/eia608/ClosedCaptionText.java | 4 +- .../exoplayer/text/eia608/Eia608Parser.java | 28 ++++++--- .../text/eia608/Eia608TrackRenderer.java | 61 +++++++++++++------ 9 files changed, 125 insertions(+), 113 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionList.java 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 b6057e7717..3c3f864276 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 @@ -17,33 +17,13 @@ package com.google.android.exoplayer.hls.parser; import com.google.android.exoplayer.util.ParsableByteArray; -import java.util.concurrent.ConcurrentLinkedQueue; - /** * Extracts individual samples from continuous byte stream, preserving original order. */ /* package */ abstract class PesPayloadReader extends SampleQueue { - private final ConcurrentLinkedQueue internalQueue; - protected PesPayloadReader(SamplePool samplePool) { super(samplePool); - internalQueue = new ConcurrentLinkedQueue(); - } - - @Override - protected final Sample internalPeekSample() { - return internalQueue.peek(); - } - - @Override - protected final Sample internalPollSample() { - return internalQueue.poll(); - } - - @Override - protected final void internalQueueSample(Sample sample) { - internalQueue.add(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 7b7916e683..1a8623468f 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 @@ -18,9 +18,12 @@ package com.google.android.exoplayer.hls.parser; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.util.ParsableByteArray; +import java.util.concurrent.ConcurrentLinkedQueue; + /* package */ abstract class SampleQueue { private final SamplePool samplePool; + private final ConcurrentLinkedQueue internalQueue; // Accessed only by the consuming thread. private boolean needKeyframe; @@ -33,6 +36,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; protected SampleQueue(SamplePool samplePool) { this.samplePool = samplePool; + internalQueue = new ConcurrentLinkedQueue(); needKeyframe = true; lastReadTimeUs = Long.MIN_VALUE; spliceOutTimeUs = Long.MIN_VALUE; @@ -66,7 +70,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; public Sample poll() { Sample head = peek(); if (head != null) { - internalPollSample(); + internalQueue.poll(); needKeyframe = false; lastReadTimeUs = head.timeUs; } @@ -79,13 +83,13 @@ import com.google.android.exoplayer.util.ParsableByteArray; * @return The next sample from the queue, or null if a sample isn't available. */ public Sample peek() { - Sample head = internalPeekSample(); + 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) { recycle(head); - internalPollSample(); - head = internalPeekSample(); + internalQueue.poll(); + head = internalQueue.peek(); } } if (head == null) { @@ -94,7 +98,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; if (spliceOutTimeUs != Long.MIN_VALUE && head.timeUs >= spliceOutTimeUs) { // The sample is later than the time this queue is spliced out. recycle(head); - internalPollSample(); + internalQueue.poll(); return null; } return head; @@ -109,8 +113,8 @@ import com.google.android.exoplayer.util.ParsableByteArray; Sample head = peek(); while (head != null && head.timeUs < timeUs) { recycle(head); - internalPollSample(); - head = internalPeekSample(); + 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; @@ -122,10 +126,10 @@ import com.google.android.exoplayer.util.ParsableByteArray; * Clears the queue. */ public void release() { - Sample toRecycle = internalPollSample(); + Sample toRecycle = internalQueue.poll(); while (toRecycle != null) { recycle(toRecycle); - toRecycle = internalPollSample(); + toRecycle = internalQueue.poll(); } } @@ -150,19 +154,19 @@ import com.google.android.exoplayer.util.ParsableByteArray; return true; } long firstPossibleSpliceTime; - Sample nextSample = internalPeekSample(); + Sample nextSample = internalQueue.peek(); if (nextSample != null) { firstPossibleSpliceTime = nextSample.timeUs; } else { firstPossibleSpliceTime = lastReadTimeUs + 1; } - Sample nextQueueSample = nextQueue.internalPeekSample(); + Sample nextQueueSample = nextQueue.internalQueue.peek(); while (nextQueueSample != null && (nextQueueSample.timeUs < firstPossibleSpliceTime || !nextQueueSample.isKeyframe)) { // Discard samples from the next queue for as long as they are before the earliest possible // splice time, or not keyframes. - nextQueue.internalPollSample(); - nextQueueSample = nextQueue.internalPeekSample(); + nextQueue.internalQueue.poll(); + nextQueueSample = nextQueue.internalQueue.peek(); } if (nextQueueSample != null) { // We've found a keyframe in the next queue that can serve as the splice point. Set the @@ -203,7 +207,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; protected void addSample(Sample sample) { largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); - internalQueueSample(sample); + internalQueue.add(sample); } protected void addToSample(Sample sample, ParsableByteArray buffer, int size) { @@ -214,8 +218,4 @@ import com.google.android.exoplayer.util.ParsableByteArray; sample.size += size; } - protected abstract Sample internalPeekSample(); - protected abstract Sample internalPollSample(); - protected abstract void internalQueueSample(Sample sample); - } 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 0d12d60de3..6d98c50a6d 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 @@ -22,28 +22,23 @@ import com.google.android.exoplayer.util.ParsableByteArray; import android.annotation.SuppressLint; -import java.util.Comparator; -import java.util.TreeSet; - /** * Parses a SEI data from H.264 frames and extracts samples with closed captions data. * * TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that * a sample with an earlier timestamp won't be added to it. */ -/* package */ class SeiReader extends SampleQueue implements Comparator { +/* package */ class SeiReader extends SampleQueue { // SEI data, used for Closed Captions. private static final int NAL_UNIT_TYPE_SEI = 6; private final ParsableByteArray seiBuffer; - private final TreeSet internalQueue; public SeiReader(SamplePool samplePool) { super(samplePool); setMediaFormat(MediaFormat.createEia608Format()); seiBuffer = new ParsableByteArray(); - internalQueue = new TreeSet(this); } @SuppressLint("InlinedApi") @@ -63,25 +58,4 @@ import java.util.TreeSet; } } - @Override - public int compare(Sample first, Sample second) { - // Note - We don't expect samples to have identical timestamps. - return first.timeUs <= second.timeUs ? -1 : 1; - } - - @Override - protected synchronized Sample internalPeekSample() { - return internalQueue.isEmpty() ? null : internalQueue.first(); - } - - @Override - protected synchronized Sample internalPollSample() { - return internalQueue.pollFirst(); - } - - @Override - protected synchronized void internalQueueSample(Sample sample) { - internalQueue.add(sample); - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java index ab6aff54c6..1961cc7a76 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer.text.eia608; /** * A Closed Caption that contains textual data associated with time indices. */ -/* package */ abstract class ClosedCaption implements Comparable { +/* package */ abstract class ClosedCaption { /** * Identifies closed captions with control characters. @@ -33,23 +33,9 @@ package com.google.android.exoplayer.text.eia608; * The type of the closed caption data. */ public final int type; - /** - * Timestamp associated with the closed caption. - */ - public final long timeUs; - protected ClosedCaption(int type, long timeUs) { + protected ClosedCaption(int type) { this.type = type; - this.timeUs = timeUs; - } - - @Override - public int compareTo(ClosedCaption another) { - long delta = this.timeUs - another.timeUs; - if (delta == 0) { - return 0; - } - return delta > 0 ? 1 : -1; } } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionCtrl.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionCtrl.java index ceca05c919..c784f50cd9 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionCtrl.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionCtrl.java @@ -70,8 +70,8 @@ package com.google.android.exoplayer.text.eia608; public final byte cc1; public final byte cc2; - protected ClosedCaptionCtrl(byte cc1, byte cc2, long timeUs) { - super(ClosedCaption.TYPE_CTRL, timeUs); + protected ClosedCaptionCtrl(byte cc1, byte cc2) { + super(ClosedCaption.TYPE_CTRL); this.cc1 = cc1; this.cc2 = cc2; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionList.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionList.java new file mode 100644 index 0000000000..f47ec1f466 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionList.java @@ -0,0 +1,39 @@ +/* + * 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.text.eia608; + +/* package */ final class ClosedCaptionList implements Comparable { + + public final long timeUs; + public final boolean decodeOnly; + public final ClosedCaption[] captions; + + public ClosedCaptionList(long timeUs, boolean decodeOnly, ClosedCaption[] captions) { + this.timeUs = timeUs; + this.decodeOnly = decodeOnly; + this.captions = captions; + } + + @Override + public int compareTo(ClosedCaptionList other) { + long delta = timeUs - other.timeUs; + if (delta == 0) { + return 0; + } + return delta > 0 ? 1 : -1; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionText.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionText.java index 49fbc5af2d..98e93ea493 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionText.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionText.java @@ -19,8 +19,8 @@ package com.google.android.exoplayer.text.eia608; public final String text; - public ClosedCaptionText(String text, long timeUs) { - super(ClosedCaption.TYPE_TEXT, timeUs); + public ClosedCaptionText(String text) { + super(ClosedCaption.TYPE_TEXT); this.text = text; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java index bce5c5de35..a855e34839 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java @@ -15,11 +15,12 @@ */ package com.google.android.exoplayer.text.eia608; +import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableByteArray; -import java.util.List; +import java.util.ArrayList; /** * Facilitates the extraction and parsing of EIA-608 (a.k.a. "line 21 captions" and "CEA-608") @@ -83,23 +84,26 @@ public class Eia608Parser { private final ParsableBitArray seiBuffer; private final StringBuilder stringBuilder; + private final ArrayList captions; /* package */ Eia608Parser() { seiBuffer = new ParsableBitArray(); stringBuilder = new StringBuilder(); + captions = new ArrayList(); } /* package */ boolean canParse(String mimeType) { return mimeType.equals(MimeTypes.APPLICATION_EIA608); } - /* package */ void parse(byte[] data, int size, long timeUs, List out) { - if (size <= 0) { - return; + /* package */ ClosedCaptionList parse(SampleHolder sampleHolder) { + if (sampleHolder.size <= 0) { + return null; } + captions.clear(); stringBuilder.setLength(0); - seiBuffer.reset(data); + seiBuffer.reset(sampleHolder.data.array()); seiBuffer.skipBits(3); // reserved + process_cc_data_flag + zero_bit int ccCount = seiBuffer.readBits(5); seiBuffer.skipBits(8); @@ -135,10 +139,10 @@ public class Eia608Parser { // Control character. if (ccData1 < 0x20) { if (stringBuilder.length() > 0) { - out.add(new ClosedCaptionText(stringBuilder.toString(), timeUs)); + captions.add(new ClosedCaptionText(stringBuilder.toString())); stringBuilder.setLength(0); } - out.add(new ClosedCaptionCtrl(ccData1, ccData2, timeUs)); + captions.add(new ClosedCaptionCtrl(ccData1, ccData2)); continue; } @@ -150,8 +154,16 @@ public class Eia608Parser { } if (stringBuilder.length() > 0) { - out.add(new ClosedCaptionText(stringBuilder.toString(), timeUs)); + captions.add(new ClosedCaptionText(stringBuilder.toString())); } + + if (captions.isEmpty()) { + return null; + } + + ClosedCaption[] captionArray = new ClosedCaption[captions.size()]; + captions.toArray(captionArray); + return new ClosedCaptionList(sampleHolder.timeUs, sampleHolder.decodeOnly, captionArray); } private static char getChar(byte ccData) { diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java index 349c1450b8..8e855bf730 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java @@ -31,8 +31,7 @@ import android.os.Looper; import android.os.Message; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.TreeSet; /** * A {@link TrackRenderer} for EIA-608 closed captions in a media stream. @@ -48,6 +47,8 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { // The default number of rows to display in roll-up captions mode. private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4; + // The maximum duration that captions are parsed ahead of the current position. + private static final int MAX_SAMPLE_READAHEAD_US = 5000000; private final SampleSource source; private final Eia608Parser eia608Parser; @@ -56,7 +57,7 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { private final MediaFormatHolder formatHolder; private final SampleHolder sampleHolder; private final StringBuilder captionStringBuilder; - private final List captionBuffer; + private final TreeSet pendingCaptionLists; private int trackIndex; private long currentPositionUs; @@ -85,7 +86,7 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { formatHolder = new MediaFormatHolder(); sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); captionStringBuilder = new StringBuilder(); - captionBuffer = new ArrayList(); + pendingCaptionLists = new TreeSet(); } @Override @@ -122,6 +123,7 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { private void seekToInternal(long positionUs) { currentPositionUs = positionUs; inputStreamEnded = false; + pendingCaptionLists.clear(); clearPendingSample(); captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; setCaptionMode(CC_MODE_UNKNOWN); @@ -138,10 +140,17 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { throw new ExoPlaybackException(e); } - if (!inputStreamEnded && !isSamplePending()) { + if (isSamplePending()) { + maybeParsePendingSample(); + } + + int result = inputStreamEnded ? SampleSource.END_OF_STREAM : SampleSource.SAMPLE_READ; + while (!isSamplePending() && result == SampleSource.SAMPLE_READ) { try { - int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); - if (result == SampleSource.END_OF_STREAM) { + result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); + if (result == SampleSource.SAMPLE_READ) { + maybeParsePendingSample(); + } else if (result == SampleSource.END_OF_STREAM) { inputStreamEnded = true; } } catch (IOException e) { @@ -149,17 +158,18 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { } } - if (isSamplePending() && sampleHolder.timeUs <= currentPositionUs) { - // Parse the pending sample. - eia608Parser.parse(sampleHolder.data.array(), sampleHolder.size, sampleHolder.timeUs, - captionBuffer); - // Consume parsed captions. - consumeCaptionBuffer(); - // Update the renderer, unless the sample was marked for decoding only. - if (!sampleHolder.decodeOnly) { + while (!pendingCaptionLists.isEmpty()) { + if (pendingCaptionLists.first().timeUs > currentPositionUs) { + // We're too early to render any of the pending caption lists. + return; + } + // Remove and consume the next caption list. + ClosedCaptionList nextCaptionList = pendingCaptionLists.pollFirst(); + consumeCaptionList(nextCaptionList); + // Update the renderer, unless the caption list was marked for decoding only. + if (!nextCaptionList.decodeOnly) { invokeRenderer(caption); } - clearPendingSample(); } } @@ -221,14 +231,26 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { textRenderer.onText(text); } - private void consumeCaptionBuffer() { - int captionBufferSize = captionBuffer.size(); + private void maybeParsePendingSample() { + if (sampleHolder.timeUs > currentPositionUs + MAX_SAMPLE_READAHEAD_US) { + // We're too early to parse the sample. + return; + } + ClosedCaptionList holder = eia608Parser.parse(sampleHolder); + clearPendingSample(); + if (holder != null) { + pendingCaptionLists.add(holder); + } + } + + private void consumeCaptionList(ClosedCaptionList captionList) { + int captionBufferSize = captionList.captions.length; if (captionBufferSize == 0) { return; } for (int i = 0; i < captionBufferSize; i++) { - ClosedCaption caption = captionBuffer.get(i); + ClosedCaption caption = captionList.captions[i]; if (caption.type == ClosedCaption.TYPE_CTRL) { ClosedCaptionCtrl captionCtrl = (ClosedCaptionCtrl) caption; if (captionCtrl.isMiscCode()) { @@ -240,7 +262,6 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { handleText((ClosedCaptionText) caption); } } - captionBuffer.clear(); if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { caption = getDisplayCaption(); From 61a86295fd3c798e24083acd228d11f832770b68 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 12 Feb 2015 17:02:34 +0000 Subject: [PATCH 07/17] Fix for video-only playbacks transitioning straight to STATE_ENDED. The complexity around not enabling the video renderer before it has a valid surface is because MediaCodecTrackRenderer supports a "discard" mode where it pulls through and discards samples without a decoder. This mode means that if the demo app were to enable the renderer before supplying the surface, the renderer could discard the first few frames prior to getting the surface, meaning video rendering wouldn't happen until the following sync frame. To get a handle on complexity, I think we're better off just removing support for this mode, which nicely decouples how the demo app handles surfaces v.s. how it handles enabling/disabling renderers. --- .../exoplayer/demo/PlayerActivity.java | 32 +++++++---------- .../exoplayer/demo/player/DemoPlayer.java | 34 +++++++++++++------ .../exoplayer/MediaCodecTrackRenderer.java | 34 +++++-------------- .../MediaCodecVideoTrackRenderer.java | 2 +- 4 files changed, 44 insertions(+), 58 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index 6c5bb7536a..6f04db8991 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -92,9 +92,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private DemoPlayer player; private boolean playerNeedsPrepare; - private boolean autoPlay = true; private long playerPosition; - private boolean enableBackgroundAudio = false; + private boolean enableBackgroundAudio; private Uri contentUri; private int contentType; @@ -166,10 +165,10 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, if (!enableBackgroundAudio) { releasePlayer(); } else { - player.blockingClearSurface(); + player.setBackgrounded(true); } - audioCapabilitiesReceiver.unregister(); + shutterView.setVisibility(View.VISIBLE); } @Override @@ -183,7 +182,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, @Override public void onClick(View view) { if (view == retryButton) { - autoPlay = true; preparePlayer(); } } @@ -192,11 +190,14 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, @Override public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { - this.audioCapabilities = audioCapabilities; - releasePlayer(); - - autoPlay = true; - preparePlayer(); + boolean audioCapabilitiesChanged = !audioCapabilities.equals(this.audioCapabilities); + if (player == null || audioCapabilitiesChanged) { + this.audioCapabilities = audioCapabilities; + releasePlayer(); + preparePlayer(); + } else if (player != null) { + player.setBackgrounded(false); + } } // Internal methods @@ -239,15 +240,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, updateButtonVisibilities(); } player.setSurface(surfaceView.getHolder().getSurface()); - maybeStartPlayback(); - } - - private void maybeStartPlayback() { - if (autoPlay && (player.getSurface().isValid() - || player.getSelectedTrackIndex(DemoPlayer.TYPE_VIDEO) == DemoPlayer.DISABLED_TRACK)) { - player.setPlayWhenReady(true); - autoPlay = false; - } + player.setPlayWhenReady(true); } private void releasePlayer() { @@ -468,7 +461,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, public void surfaceCreated(SurfaceHolder holder) { if (player != null) { player.setSurface(holder.getSurface()); - maybeStartPlayback(); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index e20b87c21a..338ec5b1db 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -179,10 +179,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private Surface surface; private InternalRendererBuilderCallback builderCallback; private TrackRenderer videoRenderer; + private int videoTrackToRestore; private MultiTrackChunkSource[] multiTrackSources; private String[][] trackNames; private int[] selectedTracks; + private boolean backgrounded; private TextListener textListener; private Id3MetadataListener id3MetadataListener; @@ -233,7 +235,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public void setSurface(Surface surface) { this.surface = surface; - pushSurfaceAndVideoTrack(false); + pushSurface(false); } public Surface getSurface() { @@ -242,7 +244,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public void blockingClearSurface() { surface = null; - pushSurfaceAndVideoTrack(true); + pushSurface(true); } public String[] getTracks(int type) { @@ -258,13 +260,23 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi return; } selectedTracks[type] = index; - if (type == TYPE_VIDEO) { - pushSurfaceAndVideoTrack(false); + pushTrackSelection(type, true); + if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) { + textListener.onText(null); + } + } + + public void setBackgrounded(boolean backgrounded) { + if (this.backgrounded == backgrounded) { + return; + } + this.backgrounded = backgrounded; + if (backgrounded) { + videoTrackToRestore = getSelectedTrackIndex(TYPE_VIDEO); + selectTrack(TYPE_VIDEO, DISABLED_TRACK); + blockingClearSurface(); } else { - pushTrackSelection(type, true); - if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) { - textListener.onText(null); - } + selectTrack(TYPE_VIDEO, videoTrackToRestore); } } @@ -307,7 +319,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi this.trackNames = trackNames; this.multiTrackSources = multiTrackSources; rendererBuildingState = RENDERER_BUILDING_STATE_BUILT; - pushSurfaceAndVideoTrack(false); + pushSurface(false); + pushTrackSelection(TYPE_VIDEO, true); pushTrackSelection(TYPE_AUDIO, true); pushTrackSelection(TYPE_TEXT, true); player.prepare(renderers); @@ -550,7 +563,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } } - private void pushSurfaceAndVideoTrack(boolean blockForSurfacePush) { + private void pushSurface(boolean blockForSurfacePush) { if (rendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { return; } @@ -562,7 +575,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi player.sendMessage( videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); } - pushTrackSelection(TYPE_VIDEO, surface != null && surface.isValid()); } private void pushTrackSelection(int type, boolean allowRendererEnable) { diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 6cbd6248a1..dd56d54050 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -440,17 +440,14 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { checkForDiscontinuity(); if (format == null) { readFormat(); - } else if (codec == null && !shouldInitCodec() && getState() == TrackRenderer.STATE_STARTED) { - discardSamples(positionUs); - } else { - if (codec == null && shouldInitCodec()) { - maybeInitCodec(); - } - if (codec != null) { - while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} - if (feedInputBuffer(true)) { - while (feedInputBuffer(false)) {} - } + } + if (codec == null && shouldInitCodec()) { + maybeInitCodec(); + } + if (codec != null) { + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} + if (feedInputBuffer(true)) { + while (feedInputBuffer(false)) {} } } codecCounters.ensureUpdated(); @@ -466,21 +463,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } } - private void discardSamples(long positionUs) throws IOException, ExoPlaybackException { - sampleHolder.data = null; - int result = SampleSource.SAMPLE_READ; - while (result == SampleSource.SAMPLE_READ && currentPositionUs <= positionUs) { - result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false); - if (result == SampleSource.SAMPLE_READ) { - if (!sampleHolder.decodeOnly) { - currentPositionUs = sampleHolder.timeUs; - } - } else if (result == SampleSource.FORMAT_READ) { - onInputFormatChanged(formatHolder); - } - } - } - private void checkForDiscontinuity() throws IOException, ExoPlaybackException { if (codec == null) { return; diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java index 397f83cb88..685b32eff3 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -353,7 +353,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { @Override protected boolean shouldInitCodec() { - return super.shouldInitCodec() && surface != null; + return super.shouldInitCodec() && surface != null && surface.isValid(); } // Override configureCodec to provide the surface. From 066334dad7f82a9e954b0ee7efd0bb0d742987b8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 12 Feb 2015 17:24:23 +0000 Subject: [PATCH 08/17] Continue TsExtractor refactor. - Remove TsExtractor's knowledge of Sample. - Push handling of Sample objects into SampleQueue as much as possible. This is a precursor to replacing Sample objects with a different type of backing memory. Ideally, the individual readers shouldn't know how the sample data is stored. This is true after this CL, with the except of the TODO in H264Reader. - Avoid double-scanning every H264 sample for NAL units, by moving the scan for SEI units from SeiReader into H264Reader. Issue: #278 --- .../exoplayer/hls/parser/AdtsReader.java | 19 +-- .../exoplayer/hls/parser/H264Reader.java | 59 +++++---- .../exoplayer/hls/parser/Id3Reader.java | 22 +-- .../exoplayer/hls/parser/SampleQueue.java | 125 ++++++++++-------- .../exoplayer/hls/parser/SeiReader.java | 28 ++-- .../exoplayer/hls/parser/TsExtractor.java | 30 +---- .../google/android/exoplayer/mp4/Mp4Util.java | 11 ++ 7 files changed, 130 insertions(+), 164 deletions(-) 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 7bea5b17c1..41b2e35b62 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 @@ -53,7 +53,6 @@ import java.util.Collections; // Used when reading the samples. private long timeUs; - private Sample currentSample; public AdtsReader(SamplePool samplePool) { super(samplePool); @@ -78,20 +77,17 @@ import java.util.Collections; int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; if (continueRead(data, adtsScratch.getData(), targetLength)) { parseHeader(); - currentSample = getSample(Sample.TYPE_AUDIO); - currentSample.timeUs = timeUs; - currentSample.isKeyframe = true; + startSample(Sample.TYPE_AUDIO, timeUs); bytesRead = 0; state = STATE_READING_SAMPLE; } break; case STATE_READING_SAMPLE: int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); - addToSample(currentSample, data, bytesToRead); + appendSampleData(data, bytesToRead); bytesRead += bytesToRead; if (bytesRead == sampleSize) { - addSample(currentSample); - currentSample = null; + commitSample(true); timeUs += frameDurationUs; bytesRead = 0; state = STATE_FINDING_SYNC; @@ -106,15 +102,6 @@ import java.util.Collections; // Do nothing. } - @Override - public void release() { - super.release(); - if (currentSample != null) { - recycle(currentSample); - currentSample = null; - } - } - /** * Continues a read from the provided {@code source} into a given {@code target}. It's assumed * that the data should be written into {@code target} starting from an offset of zero. 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 6bf1d5c4f5..2c78a0487b 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 @@ -30,14 +30,13 @@ import java.util.List; /* package */ class H264Reader extends PesPayloadReader { private static final int NAL_UNIT_TYPE_IDR = 5; + private static final int NAL_UNIT_TYPE_SEI = 6; private static final int NAL_UNIT_TYPE_SPS = 7; private static final int NAL_UNIT_TYPE_PPS = 8; private static final int NAL_UNIT_TYPE_AUD = 9; private final SeiReader seiReader; - private Sample currentSample; - public H264Reader(SamplePool samplePool, SeiReader seiReader) { super(samplePool); this.seiReader = seiReader; @@ -47,14 +46,32 @@ import java.util.List; public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { while (data.bytesLeft() > 0) { if (readToNextAudUnit(data, pesTimeUs)) { - currentSample.isKeyframe = currentSample.size - > Mp4Util.findNalUnit(currentSample.data, 0, currentSample.size, NAL_UNIT_TYPE_IDR); - if (!hasMediaFormat() && currentSample.isKeyframe) { - parseMediaFormat(currentSample); + // 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; + } } - seiReader.read(currentSample.data, currentSample.size, currentSample.timeUs); - addSample(currentSample); - currentSample = null; + + boolean isKeyframe = pendingSampleSize > idrNalUnitPosition; + if (!hasMediaFormat() && isKeyframe) { + parseMediaFormat(pendingSampleData, pendingSampleSize); + } + commitSample(isKeyframe); } } } @@ -64,15 +81,6 @@ import java.util.List; // Do nothing. } - @Override - public void release() { - super.release(); - if (currentSample != null) { - recycle(currentSample); - currentSample = null; - } - } - /** * Reads data up to (but not including) the start of the next AUD unit. * @@ -89,16 +97,15 @@ import java.util.List; int audOffset = Mp4Util.findNalUnit(data.data, pesOffset, pesLimit, NAL_UNIT_TYPE_AUD); int bytesToNextAud = audOffset - pesOffset; if (bytesToNextAud == 0) { - if (currentSample == null) { - currentSample = getSample(Sample.TYPE_VIDEO); - currentSample.timeUs = pesTimeUs; - addToSample(currentSample, data, 4); + if (!havePendingSample()) { + startSample(Sample.TYPE_VIDEO, pesTimeUs); + appendSampleData(data, 4); return false; } else { return true; } - } else if (currentSample != null) { - addToSample(currentSample, data, bytesToNextAud); + } else if (havePendingSample()) { + appendSampleData(data, bytesToNextAud); return data.bytesLeft() > 0; } else { data.skip(bytesToNextAud); @@ -106,9 +113,7 @@ import java.util.List; } } - private void parseMediaFormat(Sample sample) { - byte[] sampleData = sample.data; - int sampleSize = sample.size; + private void parseMediaFormat(byte[] sampleData, int sampleSize) { // Locate the SPS and PPS units. int spsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_SPS); int ppsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_PPS); 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 b30aead7d4..a579b47855 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 @@ -25,8 +25,6 @@ import android.annotation.SuppressLint; */ /* package */ class Id3Reader extends PesPayloadReader { - private Sample currentSample; - public Id3Reader(SamplePool samplePool) { super(samplePool); setMediaFormat(MediaFormat.createId3Format()); @@ -36,28 +34,16 @@ import android.annotation.SuppressLint; @Override public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { if (startOfPacket) { - currentSample = getSample(Sample.TYPE_MISC); - currentSample.timeUs = pesTimeUs; - currentSample.isKeyframe = true; + startSample(Sample.TYPE_MISC, pesTimeUs); } - if (currentSample != null) { - addToSample(currentSample, data, data.bytesLeft()); + if (havePendingSample()) { + appendSampleData(data, data.bytesLeft()); } } @Override public void packetFinished() { - addSample(currentSample); - currentSample = null; - } - - @Override - public void release() { - super.release(); - if (currentSample != null) { - recycle(currentSample); - currentSample = null; - } + commitSample(true); } } 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 1a8623468f..a4d418c716 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 @@ -16,8 +16,12 @@ package com.google.android.exoplayer.hls.parser; import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.util.ParsableByteArray; +import android.annotation.SuppressLint; +import android.media.MediaExtractor; + import java.util.concurrent.ConcurrentLinkedQueue; /* package */ abstract class SampleQueue { @@ -34,6 +38,10 @@ import java.util.concurrent.ConcurrentLinkedQueue; 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(); @@ -43,6 +51,10 @@ import java.util.concurrent.ConcurrentLinkedQueue; largestParsedTimestampUs = Long.MIN_VALUE; } + public boolean isEmpty() { + return peek() == null; + } + public long getLargestParsedTimestampUs() { return largestParsedTimestampUs; } @@ -60,34 +72,49 @@ import java.util.concurrent.ConcurrentLinkedQueue; } /** - * Removes and returns the next sample from the queue. + * Removes the next sample from the head of the queue, writing it into the provided holder. *

* The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples * queued prior to the first keyframe are discarded. * - * @return The next sample from the queue, or null if a sample isn't available. + * @param holder A {@link SampleHolder} into which the sample should be read. + * @return True if a sample was read. False otherwise. */ - public Sample poll() { - Sample head = peek(); - if (head != null) { - internalQueue.poll(); - needKeyframe = false; - lastReadTimeUs = head.timeUs; + @SuppressLint("InlinedApi") + public boolean getSample(SampleHolder holder) { + Sample sample = peek(); + if (sample == null) { + return false; } - return head; + // 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. + needKeyframe = false; + lastReadTimeUs = sample.timeUs; + internalQueue.poll(); + samplePool.recycle(sample); + return true; } /** - * Like {@link #poll()}, except the returned sample is not removed from the queue. + * 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. */ - public Sample peek() { + 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) { - recycle(head); + samplePool.recycle(head); internalQueue.poll(); head = internalQueue.peek(); } @@ -97,7 +124,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; } if (spliceOutTimeUs != Long.MIN_VALUE && head.timeUs >= spliceOutTimeUs) { // The sample is later than the time this queue is spliced out. - recycle(head); + samplePool.recycle(head); internalQueue.poll(); return null; } @@ -112,7 +139,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; public void discardUntil(long timeUs) { Sample head = peek(); while (head != null && head.timeUs < timeUs) { - recycle(head); + samplePool.recycle(head); internalQueue.poll(); head = internalQueue.peek(); // We're discarding at least one sample, so any subsequent read will need to start at @@ -125,21 +152,16 @@ import java.util.concurrent.ConcurrentLinkedQueue; /** * Clears the queue. */ - public void release() { + public final void release() { Sample toRecycle = internalQueue.poll(); while (toRecycle != null) { - recycle(toRecycle); + samplePool.recycle(toRecycle); toRecycle = internalQueue.poll(); } - } - - /** - * Recycles a sample. - * - * @param sample The sample to recycle. - */ - public void recycle(Sample sample) { - samplePool.recycle(sample); + if (pendingSample != null) { + samplePool.recycle(pendingSample); + pendingSample = null; + } } /** @@ -177,45 +199,34 @@ import java.util.concurrent.ConcurrentLinkedQueue; return false; } - /** - * Obtains a Sample object to use. - * - * @param type The type of the sample. - * @return The sample. - */ - protected Sample getSample(int type) { - return samplePool.get(type); + // Writing side. + + protected final boolean havePendingSample() { + return pendingSample != null; } - /** - * Creates a new Sample and adds it to the queue. - * - * @param type The type of the sample. - * @param buffer The buffer to read sample data. - * @param sampleSize The size of the sample data. - * @param sampleTimeUs The sample time stamp. - * @param isKeyframe True if the sample is a keyframe. False otherwise. - */ - protected void addSample(int type, ParsableByteArray buffer, int sampleSize, long sampleTimeUs, - boolean isKeyframe) { - Sample sample = getSample(type); - addToSample(sample, buffer, sampleSize); - sample.isKeyframe = isKeyframe; - sample.timeUs = sampleTimeUs; - addSample(sample); + protected final Sample getPendingSample() { + return pendingSample; } - protected void addSample(Sample sample) { - largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); - internalQueue.add(sample); + protected final void startSample(int type, long timeUs) { + pendingSample = samplePool.get(type); + pendingSample.timeUs = timeUs; } - protected void addToSample(Sample sample, ParsableByteArray buffer, int size) { - if (sample.data.length - sample.size < size) { - sample.expand(size - sample.data.length + sample.size); + 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(sample.data, sample.size, size); - sample.size += size; + buffer.readBytes(pendingSample.data, pendingSample.size, size); + pendingSample.size += size; + } + + protected final void commitSample(boolean isKeyframe) { + pendingSample.isKeyframe = isKeyframe; + internalQueue.add(pendingSample); + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, pendingSample.timeUs); + pendingSample = null; } } 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 6d98c50a6d..a86b44a18d 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 @@ -16,12 +16,9 @@ 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.text.eia608.Eia608Parser; import com.google.android.exoplayer.util.ParsableByteArray; -import android.annotation.SuppressLint; - /** * Parses a SEI data from H.264 frames and extracts samples with closed captions data. * @@ -30,9 +27,6 @@ import android.annotation.SuppressLint; */ /* package */ class SeiReader extends SampleQueue { - // SEI data, used for Closed Captions. - private static final int NAL_UNIT_TYPE_SEI = 6; - private final ParsableByteArray seiBuffer; public SeiReader(SamplePool samplePool) { @@ -41,20 +35,14 @@ import android.annotation.SuppressLint; seiBuffer = new ParsableByteArray(); } - @SuppressLint("InlinedApi") - public void read(byte[] data, int length, long pesTimeUs) { - seiBuffer.reset(data, length); - while (seiBuffer.bytesLeft() > 0) { - int currentOffset = seiBuffer.getPosition(); - int seiOffset = Mp4Util.findNalUnit(data, currentOffset, length, NAL_UNIT_TYPE_SEI); - if (seiOffset == length) { - return; - } - seiBuffer.skip(seiOffset + 4 - currentOffset); - int ccDataSize = Eia608Parser.parseHeader(seiBuffer); - if (ccDataSize > 0) { - addSample(Sample.TYPE_MISC, seiBuffer, ccDataSize, pesTimeUs, true); - } + public void read(byte[] data, int position, long pesTimeUs) { + seiBuffer.reset(data, data.length); + seiBuffer.setPosition(position + 4); + int ccDataSize = Eia608Parser.parseHeader(seiBuffer); + if (ccDataSize > 0) { + startSample(Sample.TYPE_MISC, 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 5f698fd6a5..9c174755c1 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 @@ -23,8 +23,6 @@ import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableByteArray; -import android.annotation.SuppressLint; -import android.media.MediaExtractor; import android.util.Log; import android.util.SparseArray; @@ -172,19 +170,12 @@ public final class TsExtractor { * Gets the next sample for the specified track. * * @param track The track from which to read. - * @param out A {@link SampleHolder} into which the next 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. */ - public boolean getSample(int track, SampleHolder out) { + public boolean getSample(int track, SampleHolder holder) { Assertions.checkState(prepared); - SampleQueue sampleQueue = sampleQueues.valueAt(track); - Sample sample = sampleQueue.poll(); - if (sample == null) { - return false; - } - convert(sample, out); - sampleQueue.recycle(sample); - return true; + return sampleQueues.valueAt(track).getSample(holder); } /** @@ -207,7 +198,7 @@ public final class TsExtractor { */ public boolean hasSamples(int track) { Assertions.checkState(prepared); - return sampleQueues.valueAt(track).peek() != null; + return !sampleQueues.valueAt(track).isEmpty(); } private boolean checkPrepared() { @@ -284,19 +275,6 @@ public final class TsExtractor { return bytesRead; } - @SuppressLint("InlinedApi") - private void convert(Sample in, SampleHolder out) { - if (out.data == null || out.data.capacity() < in.size) { - out.replaceBuffer(in.size); - } - if (out.data != null) { - out.data.put(in.data, 0, in.size); - } - out.size = in.size; - out.flags = in.isKeyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; - out.timeUs = in.timeUs; - } - /** * Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound. * diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java index 09abe2ccf1..0cc6d4d65e 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java @@ -144,4 +144,15 @@ public final class Mp4Util { return endOffset; } + /** + * Gets the type of the NAL unit in {@code data} that starts at {@code offset}. + * + * @param data The data to search. + * @param offset The start offset of a NAL unit. + * @return The type of the unit. + */ + public static int getNalUnitType(byte[] data, int offset) { + return data[offset + 3] & 0x1F; + } + } From d1360ad7d2769e0eb0c511ca93ccd58919556e8d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 13 Feb 2015 19:26:49 +0000 Subject: [PATCH 09/17] Pragmatic fix for "stuck-in-ready-state" when seeking near the end of some streams. --- .../java/com/google/android/exoplayer/audio/AudioTrack.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index 0814f6b5bb..9c33014da8 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -479,7 +479,9 @@ public final class AudioTrack { /** Returns whether enough data has been supplied via {@link #handleBuffer} to begin playback. */ public boolean hasEnoughDataToBeginPlayback() { - return submittedBytes >= minBufferSize; + // The value of minBufferSize can be slightly less than what's actually required for playback + // to start, hence the multiplication factor. + return submittedBytes > (minBufferSize * 3) / 2; } /** Sets the playback volume. */ From 321005e4b1be588366413c7537a008e066beb3b2 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 13 Feb 2015 19:27:52 +0000 Subject: [PATCH 10/17] Use 'bitrate' not 'bit-rate' consistently. --- .../android/exoplayer/mp4/CommonMp4AtomParsers.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java index 22e5b1edcb..141db60063 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java @@ -37,8 +37,8 @@ public final class CommonMp4AtomParsers { /** Channel counts for AC-3 audio, indexed by acmod. (See ETSI TS 102 366.) */ private static final int[] AC3_CHANNEL_COUNTS = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; - /** Nominal bit-rates for AC-3 audio in kbps, indexed by bit_rate_code. (See ETSI TS 102 366.) */ - private static final int[] AC3_BIT_RATES = new int[] {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, + /** Nominal bitrates for AC-3 audio in kbps, indexed by bit_rate_code. (See ETSI TS 102 366.) */ + private static final int[] AC3_BITRATES = new int[] {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640}; /** @@ -639,8 +639,8 @@ public final class CommonMp4AtomParsers { channelCount++; } - // Map bit_rate_code onto a bit-rate in kbit/s. - int bitrate = AC3_BIT_RATES[((nextByte & 0x03) << 3) + (parent.readUnsignedByte() >> 5)]; + // Map bit_rate_code onto a bitrate in kbit/s. + int bitrate = AC3_BITRATES[((nextByte & 0x03) << 3) + (parent.readUnsignedByte() >> 5)]; return new Ac3Format(channelCount, sampleRate, bitrate); } From 28166d8c0d34441ee000d624dbd86672c9810d37 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 13 Feb 2015 19:31:01 +0000 Subject: [PATCH 11/17] Rename ParsableByteArray.length() to limit(). Add capacity(). --- .../chunk/parser/mp4/FragmentedMp4Extractor.java | 4 ++-- .../exoplayer/chunk/parser/mp4/TrackFragment.java | 2 +- .../android/exoplayer/hls/parser/AdtsReader.java | 2 +- .../android/exoplayer/hls/parser/H264Reader.java | 2 +- .../android/exoplayer/util/ParsableByteArray.java | 10 +++++++--- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java index 274a4664aa..01006d469d 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java @@ -638,7 +638,7 @@ public final class FragmentedMp4Extractor implements Extractor { } Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); - out.initEncryptionData(senc.length() - senc.getPosition()); + out.initEncryptionData(senc.bytesLeft()); out.fillEncryptionData(senc); } @@ -696,7 +696,7 @@ public final class FragmentedMp4Extractor implements Extractor { offset += sizes[i]; } - return new SegmentIndex(atom.length(), sizes, offsets, durationsUs, timesUs); + return new SegmentIndex(atom.limit(), sizes, offsets, durationsUs, timesUs); } private int readEncryptionData(NonBlockingInputStream inputStream) { diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java index 9d4f8cc3de..eb16a87650 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java @@ -113,7 +113,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; * @param length The length in bytes of the encryption data. */ public void initEncryptionData(int length) { - if (sampleEncryptionData == null || sampleEncryptionData.length() < length) { + if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) { sampleEncryptionData = new ParsableByteArray(length); } sampleEncryptionDataLength = length; 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 41b2e35b62..5e1b740684 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 @@ -128,7 +128,7 @@ import java.util.Collections; private boolean skipToNextSync(ParsableByteArray pesBuffer) { byte[] adtsData = pesBuffer.data; int startOffset = pesBuffer.getPosition(); - int endOffset = pesBuffer.length(); + 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; 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 2c78a0487b..07b3e52d44 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 @@ -90,7 +90,7 @@ import java.util.List; */ private boolean readToNextAudUnit(ParsableByteArray data, long pesTimeUs) { int pesOffset = data.getPosition(); - int pesLimit = data.length(); + int pesLimit = data.limit(); // TODO: We probably need to handle the case where the AUD start code was split across the // previous and current data buffers. diff --git a/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java index 5038d4e1d6..72b2552722 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java @@ -73,9 +73,8 @@ public final class ParsableByteArray { return limit - position; } - /** Returns the number of bytes in the array. */ - // TODO: Rename to limit. - public int length() { + /** Returns the limit. */ + public int limit() { return limit; } @@ -94,6 +93,11 @@ public final class ParsableByteArray { return position; } + /** Returns the capacity of the array, which may be larger than the limit. */ + public int capacity() { + return data == null ? 0 : data.length; + } + /** * Sets the reading offset in the array. * From 37e6946cd9f4abddebba3a945811d70aa00fad02 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 13 Feb 2015 19:32:01 +0000 Subject: [PATCH 12/17] 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 --- .../android/exoplayer/hls/HlsChunkSource.java | 7 +- .../exoplayer/hls/parser/AdtsReader.java | 15 +- .../exoplayer/hls/parser/H264Reader.java | 93 ++++-- .../exoplayer/hls/parser/Id3Reader.java | 12 +- .../hls/parser/PesPayloadReader.java | 5 +- .../hls/parser/RollingSampleBuffer.java | 301 ++++++++++++++++++ .../android/exoplayer/hls/parser/Sample.java | 55 ---- .../exoplayer/hls/parser/SamplePool.java | 58 ---- .../exoplayer/hls/parser/SampleQueue.java | 196 +++++------- .../exoplayer/hls/parser/SeiReader.java | 7 +- .../exoplayer/hls/parser/TsExtractor.java | 15 +- .../exoplayer/upstream/BufferPool.java | 55 +++- 12 files changed, 518 insertions(+), 301 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/RollingSampleBuffer.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/Sample.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/SamplePool.java 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; From c3788c0931da5c63ac1dcca5b81f0303ae30323e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 13 Feb 2015 20:11:12 +0000 Subject: [PATCH 13/17] Eliminate memory copy of H264 data through H264 reader. I think this is the limit of how far we should be pushing complexity v.s. efficiency. It's a little complicated to understand, but probably worth it since the H264 bitstream is the majority of the data. Issue: #278 --- .../exoplayer/hls/parser/AdtsReader.java | 2 +- .../exoplayer/hls/parser/H264Reader.java | 249 +++++++++++------- .../exoplayer/hls/parser/Id3Reader.java | 2 +- .../hls/parser/RollingSampleBuffer.java | 27 +- .../exoplayer/hls/parser/SampleQueue.java | 20 +- .../exoplayer/hls/parser/SeiReader.java | 2 +- .../google/android/exoplayer/mp4/Mp4Util.java | 126 +++++++-- 7 files changed, 303 insertions(+), 125 deletions(-) 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 07e649979c..35813052ad 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 @@ -85,7 +85,7 @@ import java.util.Collections; break; case STATE_READING_SAMPLE: int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); - appendSampleData(data, bytesToRead); + appendData(data, bytesToRead); bytesRead += bytesToRead; if (bytesRead == sampleSize) { commitSample(true); 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 33ca516c4a..00d80ba6bb 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 @@ -18,11 +18,13 @@ 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.Assertions; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableByteArray; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -37,54 +39,72 @@ import java.util.List; private static final int NAL_UNIT_TYPE_AUD = 9; private final SeiReader seiReader; - private final ParsableByteArray pendingSampleWrapper; + private final boolean[] prefixFlags; + private final NalUnitTargetBuffer sps; + private final NalUnitTargetBuffer pps; + private final NalUnitTargetBuffer sei; - // 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; + private boolean isKeyframe; public H264Reader(BufferPool bufferPool, SeiReader seiReader) { super(bufferPool); this.seiReader = seiReader; - this.pendingSampleData = new byte[1024]; - this.pendingSampleWrapper = new ParsableByteArray(); + prefixFlags = new boolean[3]; + sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); + pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); + sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); } @Override public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { while (data.bytesLeft() > 0) { - boolean sampleFinished = readToNextAudUnit(data, pesTimeUs); - if (!sampleFinished) { - continue; - } + int offset = data.getPosition(); + int limit = data.limit(); + byte[] dataArray = data.data; - // 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); + // Append the data to the buffer. + appendData(data, data.bytesLeft()); + + // Scan the appended data, processing NAL units as they are encountered + while (offset < limit) { + int nextNalUnitOffset = Mp4Util.findNalUnit(dataArray, offset, limit, prefixFlags); + if (nextNalUnitOffset < limit) { + // We've seen the start of a NAL unit. + + // This is the length to the start of the unit. It may be negative if the NAL unit + // actually started in previously consumed data. + int lengthToNalUnit = nextNalUnitOffset - offset; + if (lengthToNalUnit > 0) { + feedNalUnitTargetBuffersData(dataArray, offset, nextNalUnitOffset); } - position += 4; + + int nalUnitType = Mp4Util.getNalUnitType(dataArray, nextNalUnitOffset); + int nalUnitOffsetInData = nextNalUnitOffset - limit; + if (nalUnitType == NAL_UNIT_TYPE_AUD) { + if (writingSample()) { + if (isKeyframe && !hasMediaFormat() && sps.isCompleted() && pps.isCompleted()) { + parseMediaFormat(sps, pps); + } + commitSample(isKeyframe, nalUnitOffsetInData); + } + startSample(pesTimeUs, nalUnitOffsetInData); + isKeyframe = false; + } else if (nalUnitType == NAL_UNIT_TYPE_IDR) { + isKeyframe = true; + } + + // If the length to the start of the unit is negative then we wrote too many bytes to the + // NAL buffers. Discard the excess bytes when notifying that the unit has ended. + feedNalUnitTargetEnd(pesTimeUs, lengthToNalUnit < 0 ? -lengthToNalUnit : 0); + // Notify the start of the next NAL unit. + feedNalUnitTargetBuffersStart(nalUnitType); + // Continue scanning the data. + offset = nextNalUnitOffset + 4; + } else { + feedNalUnitTargetBuffersData(dataArray, offset, limit); + offset = limit; } } - - // 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); } } @@ -93,71 +113,41 @@ import java.util.List; // Do nothing. } - /** - * Reads data up to (but not including) the start of the next AUD unit. - * - * @param data The data to consume. - * @param pesTimeUs The corresponding time. - * @return True if the current sample is now complete. False otherwise. - */ - private boolean readToNextAudUnit(ParsableByteArray data, long pesTimeUs) { - int pesOffset = data.getPosition(); - int pesLimit = data.limit(); + private void feedNalUnitTargetBuffersStart(int nalUnitType) { + if (!hasMediaFormat()) { + sps.startNalUnit(nalUnitType); + pps.startNalUnit(nalUnitType); + } + sei.startNalUnit(nalUnitType); + } - // TODO: We probably need to handle the case where the AUD start code was split across the - // previous and current data buffers. - int audOffset = Mp4Util.findNalUnit(data.data, pesOffset, pesLimit, NAL_UNIT_TYPE_AUD); - int bytesToNextAud = audOffset - pesOffset; - if (bytesToNextAud == 0) { - if (!writingSample()) { - startSample(pesTimeUs); - pendingSampleSize = 0; - pendingSampleTimeUs = pesTimeUs; - appendToSample(data, 4); - return false; - } else { - return true; - } - } else if (writingSample()) { - appendToSample(data, bytesToNextAud); - return data.bytesLeft() > 0; - } else { - data.skip(bytesToNextAud); - return false; + private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) { + if (!hasMediaFormat()) { + sps.appendToNalUnit(dataArray, offset, limit); + pps.appendToNalUnit(dataArray, offset, limit); + } + sei.appendToNalUnit(dataArray, offset, limit); + } + + private void feedNalUnitTargetEnd(long pesTimeUs, int discardPadding) { + sps.endNalUnit(discardPadding); + pps.endNalUnit(discardPadding); + if (sei.endNalUnit(discardPadding)) { + seiReader.read(sei.nalData, 0, pesTimeUs); } } - 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); - int ppsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_PPS); - if (spsOffset == sampleSize || ppsOffset == sampleSize) { - return; - } - // Determine the length of the units, and copy them to build the initialization data. - int spsLength = Mp4Util.findNalUnit(sampleData, spsOffset + 3, sampleSize) - spsOffset; - int ppsLength = Mp4Util.findNalUnit(sampleData, ppsOffset + 3, sampleSize) - ppsOffset; - byte[] spsData = new byte[spsLength]; - byte[] ppsData = new byte[ppsLength]; - System.arraycopy(sampleData, spsOffset, spsData, 0, spsLength); - System.arraycopy(sampleData, ppsOffset, ppsData, 0, ppsLength); + private void parseMediaFormat(NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { + byte[] spsData = new byte[sps.nalLength]; + byte[] ppsData = new byte[pps.nalLength]; + System.arraycopy(sps.nalData, 0, spsData, 0, sps.nalLength); + System.arraycopy(pps.nalData, 0, ppsData, 0, pps.nalLength); List initializationData = new ArrayList(); initializationData.add(spsData); initializationData.add(ppsData); // Unescape and then parse the SPS unit. - byte[] unescapedSps = unescapeStream(spsData, 0, spsLength); + byte[] unescapedSps = unescapeStream(spsData, 0, spsData.length); ParsableBitArray bitArray = new ParsableBitArray(unescapedSps); bitArray.skipBits(32); // NAL header int profileIdc = bitArray.readBits(8); @@ -293,4 +283,83 @@ import java.util.List; return limit; } + /** + * A buffer that fills itself with data corresponding to a specific NAL unit, as it is + * encountered in the stream. + */ + private static final class NalUnitTargetBuffer { + + private final int targetType; + + private boolean isFilling; + private boolean isCompleted; + + public byte[] nalData; + public int nalLength; + + public NalUnitTargetBuffer(int targetType, int initialCapacity) { + this.targetType = targetType; + // Initialize data, writing the known NAL prefix into the first four bytes. + nalData = new byte[4 + initialCapacity]; + nalData[2] = 1; + nalData[3] = (byte) targetType; + } + + public boolean isCompleted() { + return isCompleted; + } + + /** + * Invoked to indicate that a NAL unit has started. + * + * @param type The type of the NAL unit. + */ + public void startNalUnit(int type) { + Assertions.checkState(!isFilling); + isFilling = type == targetType; + if (isFilling) { + // Length is initially the length of the NAL prefix. + nalLength = 4; + isCompleted = false; + } + } + + /** + * Invoked to pass stream data. The data passed should not include 4 byte NAL unit prefixes. + * + * @param data Holds the data being passed. + * @param offset The offset of the data in {@code data}. + * @param limit The limit (exclusive) of the data in {@code data}. + */ + public void appendToNalUnit(byte[] data, int offset, int limit) { + if (!isFilling) { + return; + } + int readLength = limit - offset; + if (nalData.length < nalLength + readLength) { + nalData = Arrays.copyOf(nalData, (nalLength + readLength) * 2); + } + System.arraycopy(data, offset, nalData, nalLength, readLength); + nalLength += readLength; + } + + /** + * Invoked to indicate that a NAL unit has ended. + * + * @param discardPadding The number of excess bytes that were passed to + * {@link #appendData(byte[], int, int)}, which should be discarded. + * @return True if the ended NAL unit is of the target type. False otherwise. + */ + public boolean endNalUnit(int discardPadding) { + if (!isFilling) { + return false; + } + nalLength -= discardPadding; + isFilling = false; + isCompleted = true; + return true; + } + + } + } 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 10229200d9..609337b664 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 @@ -35,7 +35,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; startSample(pesTimeUs); } if (writingSample()) { - appendSampleData(data, data.bytesLeft()); + appendData(data, data.bytesLeft()); } } 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 index 032c7946d2..2971370886 100644 --- 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 @@ -17,6 +17,7 @@ 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.Assertions; import com.google.android.exoplayer.util.ParsableByteArray; import android.annotation.SuppressLint; @@ -44,7 +45,6 @@ import java.util.concurrent.ConcurrentLinkedQueue; private long totalBytesWritten; private byte[] lastFragment; private int lastFragmentOffset; - private int pendingSampleSize; private long pendingSampleTimeUs; private long pendingSampleOffset; @@ -141,23 +141,25 @@ import java.util.concurrent.ConcurrentLinkedQueue; // Called by the loading thread. /** - * Starts writing the next sample. + * Indicates the start point for the next sample. * * @param sampleTimeUs The sample timestamp. + * @param offset The offset of the sample's data, relative to the total number of bytes written + * to the buffer. Must be negative or zero. */ - public void startSample(long sampleTimeUs) { + public void startSample(long sampleTimeUs, int offset) { + Assertions.checkState(offset <= 0); pendingSampleTimeUs = sampleTimeUs; - pendingSampleOffset = totalBytesWritten; - pendingSampleSize = 0; + pendingSampleOffset = totalBytesWritten + offset; } /** - * Appends data to the sample currently being written. + * Appends data to the rolling buffer. * * @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) { + public void appendData(ParsableByteArray buffer, int length) { int remainingWriteLength = length; while (remainingWriteLength > 0) { if (dataQueue.isEmpty() || lastFragmentOffset == fragmentLength) { @@ -171,17 +173,20 @@ import java.util.concurrent.ConcurrentLinkedQueue; remainingWriteLength -= thisWriteLength; } totalBytesWritten += length; - pendingSampleSize += length; } /** - * Commits the sample currently being written, making it available for consumption. + * Indicates the end point for the current sample, making it available for consumption. * * @param isKeyframe True if the sample being committed is a keyframe. False otherwise. + * @param offset The offset of the first byte after the end of the sample's data, relative to + * the total number of bytes written to the buffer. Must be negative or zero. */ @SuppressLint("InlinedApi") - public void commitSample(boolean isKeyframe) { - infoQueue.commitSample(pendingSampleTimeUs, pendingSampleOffset, pendingSampleSize, + public void commitSample(boolean isKeyframe, int offset) { + Assertions.checkState(offset <= 0); + int sampleSize = (int) (totalBytesWritten + offset - pendingSampleOffset); + infoQueue.commitSample(pendingSampleTimeUs, pendingSampleOffset, sampleSize, isKeyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0); } 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 d372b10f23..18cb3226a5 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 @@ -181,17 +181,25 @@ import android.media.MediaExtractor; } protected void startSample(long sampleTimeUs) { - writingSample = true; - largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs); - rollingBuffer.startSample(sampleTimeUs); + startSample(sampleTimeUs, 0); } - protected void appendSampleData(ParsableByteArray buffer, int size) { - rollingBuffer.appendSampleData(buffer, size); + protected void startSample(long sampleTimeUs, int offset) { + writingSample = true; + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs); + rollingBuffer.startSample(sampleTimeUs, offset); + } + + protected void appendData(ParsableByteArray buffer, int length) { + rollingBuffer.appendData(buffer, length); } protected void commitSample(boolean isKeyframe) { - rollingBuffer.commitSample(isKeyframe); + commitSample(isKeyframe, 0); + } + + protected void commitSample(boolean isKeyframe, int offset) { + rollingBuffer.commitSample(isKeyframe, offset); 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 de20e88a7e..6da719ae22 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 @@ -42,7 +42,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; int ccDataSize = Eia608Parser.parseHeader(seiBuffer); if (ccDataSize > 0) { startSample(pesTimeUs); - appendSampleData(seiBuffer, ccDataSize); + appendData(seiBuffer, ccDataSize); commitSample(true); } } diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java index 0cc6d4d65e..7b744dfea6 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.mp4; +import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.ParsableByteArray; @@ -99,18 +100,6 @@ public final class Mp4Util { return CodecSpecificDataUtil.buildNalUnit(atom.data, offset, length); } - /** - * Like {@link #findNalUnit(byte[], int, int, int)} with {@code type == -1}. - * - * @param data The data to search. - * @param startOffset The offset (inclusive) in the data to start the search. - * @param endOffset The offset (exclusive) in the data to end the search. - * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. - */ - public static int findNalUnit(byte[] data, int startOffset, int endOffset) { - return findNalUnit(data, startOffset, endOffset, -1); - } - /** * Finds the first NAL unit in {@code data}. *

@@ -124,6 +113,54 @@ public final class Mp4Util { * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. */ public static int findNalUnit(byte[] data, int startOffset, int endOffset, int type) { + return findNalUnit(data, startOffset, endOffset, type, null); + } + + /** + * Like {@link #findNalUnit(byte[], int, int, int)}, but supports finding of NAL units across + * array boundaries. + *

+ * To use this method, pass the same {@code prefixFlags} parameter to successive calls where the + * data passed represents a contiguous stream. The state maintained in this parameter allows the + * detection of NAL units where the NAL unit prefix spans array boundaries. + *

+ * Note that when using {@code prefixFlags} the return value may be 3, 2 or 1 less than + * {@code startOffset}, to indicate a NAL unit starting 3, 2 or 1 bytes before the first byte in + * the current array. + * + * @param data The data to search. + * @param startOffset The offset (inclusive) in the data to start the search. + * @param endOffset The offset (exclusive) in the data to end the search. + * @param type The type of the NAL unit to search for, or -1 for any NAL unit. + * @param prefixFlags A boolean array whose first three elements are used to store the state + * required to detect NAL units where the NAL unit prefix spans array boundaries. The array + * must be at least 3 elements long. + * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. + */ + public static int findNalUnit(byte[] data, int startOffset, int endOffset, int type, + boolean[] prefixFlags) { + int length = endOffset - startOffset; + + Assertions.checkState(length >= 0); + if (length == 0) { + return endOffset; + } + + if (prefixFlags != null) { + if (prefixFlags[0] && matchesType(data, startOffset, type)) { + clearPrefixFlags(prefixFlags); + return startOffset - 3; + } else if (length > 1 && prefixFlags[1] && data[startOffset] == 1 + && matchesType(data, startOffset + 1, type)) { + clearPrefixFlags(prefixFlags); + return startOffset - 2; + } else if (length > 2 && prefixFlags[2] && data[startOffset] == 0 + && data[startOffset + 1] == 1 && matchesType(data, startOffset + 2, type)) { + clearPrefixFlags(prefixFlags); + return startOffset - 1; + } + } + int limit = endOffset - 2; // We're looking for the NAL unit start code prefix 0x000001, followed by a byte that matches // the specified type. The value of i tracks the index of the third byte in the four bytes @@ -132,8 +169,8 @@ public final class Mp4Util { if ((data[i] & 0xFE) != 0) { // There isn't a NAL prefix here, or at the next two positions. Do nothing and let the // loop advance the index by three. - } else if ((data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1) - && (type == -1 || (type == (data[i + 1] & 0x1F)))) { + } else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1 + && matchesType(data, i + 1, type)) { return i - 2; } else { // There isn't a NAL prefix here, but there might be at the next position. We should @@ -141,18 +178,77 @@ public final class Mp4Util { i -= 2; } } + + if (prefixFlags != null) { + // True if the last three bytes in the data seen so far are {0,0,1}. + prefixFlags[0] = length > 2 + ? (data[endOffset - 3] == 0 && data[endOffset - 2] == 0 && data[endOffset - 1] == 1) + : length == 2 ? (prefixFlags[2] && data[endOffset - 2] == 0 && data[endOffset - 1] == 1) + : (prefixFlags[1] && data[endOffset - 1] == 1); + // True if the last three bytes in the data seen so far are {0,0}. + prefixFlags[1] = length > 1 ? data[endOffset - 2] == 0 && data[endOffset - 1] == 0 + : prefixFlags[2] && data[endOffset - 1] == 0; + // True if the last three bytes in the data seen so far are {0}. + prefixFlags[2] = data[endOffset - 1] == 0; + } + return endOffset; } + /** + * Like {@link #findNalUnit(byte[], int, int, int)} with {@code type == -1}. + * + * @param data The data to search. + * @param startOffset The offset (inclusive) in the data to start the search. + * @param endOffset The offset (exclusive) in the data to end the search. + * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. + */ + public static int findNalUnit(byte[] data, int startOffset, int endOffset) { + return findNalUnit(data, startOffset, endOffset, null); + } + + /** + * Like {@link #findNalUnit(byte[], int, int, int, boolean[])} with {@code type == -1}. + * + * @param data The data to search. + * @param startOffset The offset (inclusive) in the data to start the search. + * @param endOffset The offset (exclusive) in the data to end the search. + * @param prefixFlags A boolean array of length at least 3. + * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. + */ + public static int findNalUnit(byte[] data, int startOffset, int endOffset, + boolean[] prefixFlags) { + return findNalUnit(data, startOffset, endOffset, -1, prefixFlags); + } + /** * Gets the type of the NAL unit in {@code data} that starts at {@code offset}. * * @param data The data to search. - * @param offset The start offset of a NAL unit. + * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and + * {@code data.length - 3} (exclusive). * @return The type of the unit. */ public static int getNalUnitType(byte[] data, int offset) { return data[offset + 3] & 0x1F; } + /** + * Clears prefix flags, as used by {@link #findNalUnit(byte[], int, int, int, boolean[])}. + * + * @param prefixFlags The flags to clear. + */ + private static void clearPrefixFlags(boolean[] prefixFlags) { + prefixFlags[0] = false; + prefixFlags[1] = false; + prefixFlags[2] = false; + } + + /** + * Returns true if the type at {@code offset} is equal to {@code type}, or if {@code type == -1}. + */ + private static boolean matchesType(byte[] data, int offset, int type) { + return type == -1 || (data[offset] & 0x1F) == type; + } + } From 5f64a1fde75824331c9c9294b7116eb2bb5f033a Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 13 Feb 2015 20:13:11 +0000 Subject: [PATCH 14/17] Restrict HLS buffering by total buffer size as well as duration. This prevents excessive memory consumption when switching to very high bitrate streams. Issue: #278 --- .../android/exoplayer/hls/HlsChunkSource.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) 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 ee9b7c6cca..90bc497478 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 @@ -82,6 +82,11 @@ public class HlsChunkSource { */ public static final int ADAPTIVE_MODE_ABRUPT = 3; + /** + * The default target buffer size in bytes. + */ + public static final int DEFAULT_TARGET_BUFFER_SIZE = 18 * 1024 * 1024; + /** * The default target buffer duration in milliseconds. */ @@ -111,6 +116,7 @@ public class HlsChunkSource { private final Uri baseUri; private final int maxWidth; private final int maxHeight; + private final int targetBufferSize; private final long targetBufferDurationUs; private final long minBufferDurationToSwitchUpUs; private final long maxBufferDurationToSwitchDownUs; @@ -131,8 +137,8 @@ public class HlsChunkSource { public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) { this(dataSource, playlistUrl, playlist, bandwidthMeter, variantIndices, adaptiveMode, - DEFAULT_TARGET_BUFFER_DURATION_MS, DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, - DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS); + DEFAULT_TARGET_BUFFER_SIZE, DEFAULT_TARGET_BUFFER_DURATION_MS, + DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS); } /** @@ -145,9 +151,10 @@ public class HlsChunkSource { * @param adaptiveMode The mode for switching from one variant to another. One of * {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and * {@link #ADAPTIVE_MODE_SPLICE}. + * @param targetBufferSize The targeted buffer size in bytes. The buffer will not be filled more + * than one chunk beyond this amount of data. * @param targetBufferDurationMs The targeted duration of media to buffer ahead of the current - * playback position. Note that the greater this value, the greater the amount of memory - * that will be consumed. + * playback position. The buffer will not be filled more than one chunk beyond this position. * @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered * for a switch to a higher quality variant to be considered. * @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered @@ -155,11 +162,12 @@ public class HlsChunkSource { */ public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode, - long targetBufferDurationMs, long minBufferDurationToSwitchUpMs, + int targetBufferSize, long targetBufferDurationMs, long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs) { this.upstreamDataSource = dataSource; this.bandwidthMeter = bandwidthMeter; this.adaptiveMode = adaptiveMode; + this.targetBufferSize = targetBufferSize; targetBufferDurationUs = targetBufferDurationMs * 1000; minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000; maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000; @@ -226,8 +234,9 @@ public class HlsChunkSource { public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs, long playbackPositionUs) { if (previousTsChunk != null && (previousTsChunk.isLastChunk - || previousTsChunk.endTimeUs - playbackPositionUs >= targetBufferDurationUs)) { - // We're either finished, or we have the target amount of data buffered. + || previousTsChunk.endTimeUs - playbackPositionUs >= targetBufferDurationUs) + || bufferPool.getAllocatedSize() >= targetBufferSize) { + // We're either finished, or we have the target amount of data or time buffered. return null; } From bc99435b4855bf3d776005de6e81e9f13e1f8f8d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 13 Feb 2015 20:20:45 +0000 Subject: [PATCH 15/17] Add C.SAMPLE_FLAG_SYNC and removed InlinedApi suppression in a few places. --- .../src/main/java/com/google/android/exoplayer/C.java | 8 ++++++++ .../android/exoplayer/MediaCodecTrackRenderer.java | 2 +- .../chunk/parser/mp4/FragmentedMp4Extractor.java | 3 +-- .../exoplayer/chunk/parser/webm/WebmExtractor.java | 6 +----- .../android/exoplayer/hls/parser/SampleQueue.java | 10 +++------- .../android/exoplayer/mp4/CommonMp4AtomParsers.java | 7 ++----- .../android/exoplayer/mp4/Mp4TrackSampleTable.java | 7 +++---- 7 files changed, 19 insertions(+), 24 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java index f710b0d7a7..6bf5e08c42 100644 --- a/library/src/main/java/com/google/android/exoplayer/C.java +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer; +import android.media.MediaExtractor; + /** * Defines constants that are generally useful throughout the library. */ @@ -40,6 +42,12 @@ public final class C { */ public static final String UTF8_NAME = "UTF-8"; + /** + * Sample flag that indicates the sample is a synchronization sample. + */ + @SuppressWarnings("InlinedApi") + public static final int SAMPLE_FLAG_SYNC = MediaExtractor.SAMPLE_FLAG_SYNC; + private C() {} } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index dd56d54050..61b12e33b1 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -572,7 +572,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { if (waitingForFirstSyncFrame) { // TODO: Find out if it's possible to supply samples prior to the first sync // frame for HE-AAC. - if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) == 0) { + if ((sampleHolder.flags & C.SAMPLE_FLAG_SYNC) == 0) { sampleHolder.data.clear(); if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { // The buffer we just cleared contained reconfiguration data. We need to re-write this diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java index 01006d469d..d6229cf805 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java @@ -762,7 +762,6 @@ public final class FragmentedMp4Extractor implements Extractor { return 0; } - @SuppressLint("InlinedApi") private int readSample(NonBlockingInputStream inputStream, int sampleSize, SampleHolder out) { if (out == null) { return RESULT_NEED_SAMPLE_HOLDER; @@ -770,7 +769,7 @@ public final class FragmentedMp4Extractor implements Extractor { out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L; out.flags = 0; if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) { - out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC; + out.flags |= C.SAMPLE_FLAG_SYNC; lastSyncSampleIndex = sampleIndex; } if (out.data == null || out.data.capacity() < sampleSize) { diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java index 829d604a77..ddf15e5610 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java @@ -25,9 +25,6 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.util.LongArray; import com.google.android.exoplayer.util.MimeTypes; -import android.annotation.TargetApi; -import android.media.MediaExtractor; - import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -42,7 +39,6 @@ import java.util.concurrent.TimeUnit; * Matroska is available here. * More info about WebM is here. */ -@TargetApi(16) public final class WebmExtractor implements Extractor { private static final String DOC_TYPE_WEBM = "webm"; @@ -412,7 +408,7 @@ public final class WebmExtractor implements Extractor { case LACING_NONE: long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes; simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs; - sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; + sampleHolder.flags = keyframe ? C.SAMPLE_FLAG_SYNC : 0; sampleHolder.decodeOnly = invisible; sampleHolder.timeUs = clusterTimecodeUs + timecodeUs; sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead()); 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 18cb3226a5..348a2cd843 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 @@ -15,14 +15,12 @@ */ 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.util.ParsableByteArray; -import android.annotation.SuppressLint; -import android.media.MediaExtractor; - /** * 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 @@ -117,7 +115,6 @@ import android.media.MediaExtractor; * @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. @@ -132,7 +129,7 @@ import android.media.MediaExtractor; RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer; while (nextRollingBuffer.peekSample(sampleInfoHolder) && (sampleInfoHolder.timeUs < firstPossibleSpliceTime - || (sampleInfoHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) == 0)) { + || (sampleInfoHolder.flags & C.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. nextRollingBuffer.skipSample(); @@ -152,11 +149,10 @@ import android.media.MediaExtractor; * @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) { + while (haveNext && (sampleInfoHolder.flags & C.SAMPLE_FLAG_SYNC) == 0) { rollingBuffer.skipSample(); haveNext = rollingBuffer.peekSample(sampleInfoHolder); } diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java index 141db60063..4443e573c9 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java @@ -24,8 +24,6 @@ import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.Util; -import android.annotation.SuppressLint; -import android.media.MediaExtractor; import android.util.Pair; import java.util.ArrayList; @@ -81,7 +79,6 @@ public final class CommonMp4AtomParsers { * @param stblAtom stbl (sample table) atom to parse. * @return Sample table described by the stbl atom. */ - @SuppressLint("InlinedApi") public static Mp4TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom) { // Array of sample sizes. ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data; @@ -174,9 +171,9 @@ public final class CommonMp4AtomParsers { timestamps[i] = timestampTimeUnits + timestampOffset; // All samples are synchronization samples if the stss is not present. - flags[i] = stss == null ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; + flags[i] = stss == null ? C.SAMPLE_FLAG_SYNC : 0; if (i == nextSynchronizationSampleIndex) { - flags[i] = MediaExtractor.SAMPLE_FLAG_SYNC; + flags[i] = C.SAMPLE_FLAG_SYNC; remainingSynchronizationSamples--; if (remainingSynchronizationSamples > 0) { nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1; diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java index 325247edf1..9e60146140 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java @@ -15,11 +15,10 @@ */ package com.google.android.exoplayer.mp4; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; -import android.media.MediaExtractor; - /** Sample table for a track in an MP4 file. */ public final class Mp4TrackSampleTable { @@ -59,7 +58,7 @@ public final class Mp4TrackSampleTable { public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) { int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false); for (int i = startIndex; i >= 0; i--) { - if (timestampsUs[i] <= timeUs && (flags[i] & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) { + if (timestampsUs[i] <= timeUs && (flags[i] & C.SAMPLE_FLAG_SYNC) != 0) { return i; } } @@ -77,7 +76,7 @@ public final class Mp4TrackSampleTable { public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) { int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false); for (int i = startIndex; i < timestampsUs.length; i++) { - if (timestampsUs[i] >= timeUs && (flags[i] & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) { + if (timestampsUs[i] >= timeUs && (flags[i] & C.SAMPLE_FLAG_SYNC) != 0) { return i; } } From 9d480ecd204cfd2cd3585c28765f420e7384c162 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 13 Feb 2015 20:34:30 +0000 Subject: [PATCH 16/17] Fix broken javadoc link --- .../com/google/android/exoplayer/hls/parser/H264Reader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 00d80ba6bb..55faeefcf4 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 @@ -347,7 +347,7 @@ import java.util.List; * Invoked to indicate that a NAL unit has ended. * * @param discardPadding The number of excess bytes that were passed to - * {@link #appendData(byte[], int, int)}, which should be discarded. + * {@link #appendToNalUnit(byte[], int, int)}, which should be discarded. * @return True if the ended NAL unit is of the target type. False otherwise. */ public boolean endNalUnit(int discardPadding) { From 40411269daef787f52cab19b5c0bdb884faefd4d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 13 Feb 2015 20:36:27 +0000 Subject: [PATCH 17/17] Use C.SAMPLE_FLAG_SYNC --- .../android/exoplayer/hls/parser/RollingSampleBuffer.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 index 2971370886..48a618edde 100644 --- 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 @@ -15,14 +15,12 @@ */ package com.google.android.exoplayer.hls.parser; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.ParsableByteArray; -import android.annotation.SuppressLint; -import android.media.MediaExtractor; - import java.nio.ByteBuffer; import java.util.concurrent.ConcurrentLinkedQueue; @@ -182,12 +180,11 @@ import java.util.concurrent.ConcurrentLinkedQueue; * @param offset The offset of the first byte after the end of the sample's data, relative to * the total number of bytes written to the buffer. Must be negative or zero. */ - @SuppressLint("InlinedApi") public void commitSample(boolean isKeyframe, int offset) { Assertions.checkState(offset <= 0); int sampleSize = (int) (totalBytesWritten + offset - pendingSampleOffset); infoQueue.commitSample(pendingSampleTimeUs, pendingSampleOffset, sampleSize, - isKeyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0); + isKeyframe ? C.SAMPLE_FLAG_SYNC : 0); } /**