Commit 7c66b6ed by Oliver Woodman

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
parent b0a3c30a
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.demo" package="com.google.android.exoplayer.demo"
android:versionCode="1100" android:versionCode="1200"
android:versionName="1.1.00" android:versionName="1.2.00"
android:theme="@style/RootTheme"> android:theme="@style/RootTheme">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
......
...@@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo { ...@@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo {
/** /**
* The version of the library, expressed as a string. * 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. * The version of the library, expressed as an integer.
...@@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo { ...@@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo {
* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
* corresponding integer version 001002003. * 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} * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}
......
...@@ -17,19 +17,21 @@ package com.google.android.exoplayer.hls; ...@@ -17,19 +17,21 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.BitArray;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
/** /**
* An abstract base class for {@link HlsChunk} implementations where the data should be loaded into * 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 static final int READ_GRANULARITY = 16 * 1024;
private final BitArray bitArray; private byte[] data;
private int limit;
private volatile boolean loadFinished; private volatile boolean loadFinished;
private volatile boolean loadCanceled; private volatile boolean loadCanceled;
...@@ -39,26 +41,27 @@ public abstract class BitArrayChunk extends HlsChunk { ...@@ -39,26 +41,27 @@ public abstract class BitArrayChunk extends HlsChunk {
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed * the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}. * {@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); super(dataSource, dataSpec);
this.bitArray = bitArray; this.data = data;
} }
@Override @Override
public void consume() throws IOException { public void consume() throws IOException {
consume(bitArray); consume(data, limit);
} }
/** /**
* Invoked by {@link #consume()}. Implementations should override this method to consume the * Invoked by {@link #consume()}. Implementations should override this method to consume the
* loaded data. * 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. * @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. * Whether the whole of the chunk has been loaded.
...@@ -85,11 +88,15 @@ public abstract class BitArrayChunk extends HlsChunk { ...@@ -85,11 +88,15 @@ public abstract class BitArrayChunk extends HlsChunk {
@Override @Override
public final void load() throws IOException, InterruptedException { public final void load() throws IOException, InterruptedException {
try { try {
bitArray.reset();
dataSource.open(dataSpec); dataSource.open(dataSpec);
limit = 0;
int bytesRead = 0; int bytesRead = 0;
while (bytesRead != -1 && !loadCanceled) { 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; loadFinished = !loadCanceled;
} finally { } finally {
...@@ -97,4 +104,14 @@ public abstract class BitArrayChunk extends HlsChunk { ...@@ -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);
}
}
} }
...@@ -24,7 +24,6 @@ import com.google.android.exoplayer.upstream.DataSource; ...@@ -24,7 +24,6 @@ import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.BitArray;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.net.Uri; import android.net.Uri;
...@@ -35,6 +34,7 @@ import java.io.ByteArrayInputStream; ...@@ -35,6 +34,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
...@@ -106,7 +106,6 @@ public class HlsChunkSource { ...@@ -106,7 +106,6 @@ public class HlsChunkSource {
private final HlsPlaylistParser playlistParser; private final HlsPlaylistParser playlistParser;
private final Variant[] enabledVariants; private final Variant[] enabledVariants;
private final BandwidthMeter bandwidthMeter; private final BandwidthMeter bandwidthMeter;
private final BitArray bitArray;
private final int adaptiveMode; private final int adaptiveMode;
private final Uri baseUri; private final Uri baseUri;
private final int maxWidth; private final int maxWidth;
...@@ -115,6 +114,7 @@ public class HlsChunkSource { ...@@ -115,6 +114,7 @@ public class HlsChunkSource {
private final long minBufferDurationToSwitchUpUs; private final long minBufferDurationToSwitchUpUs;
private final long maxBufferDurationToSwitchDownUs; private final long maxBufferDurationToSwitchDownUs;
/* package */ byte[] scratchSpace;
/* package */ final HlsMediaPlaylist[] mediaPlaylists; /* package */ final HlsMediaPlaylist[] mediaPlaylists;
/* package */ final boolean[] mediaPlaylistBlacklistFlags; /* package */ final boolean[] mediaPlaylistBlacklistFlags;
/* package */ final long[] lastMediaPlaylistLoadTimesMs; /* package */ final long[] lastMediaPlaylistLoadTimesMs;
...@@ -163,7 +163,6 @@ public class HlsChunkSource { ...@@ -163,7 +163,6 @@ public class HlsChunkSource {
minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000; minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000;
maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000; maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000;
baseUri = playlist.baseUri; baseUri = playlist.baseUri;
bitArray = new BitArray();
playlistParser = new HlsPlaylistParser(); playlistParser = new HlsPlaylistParser();
if (playlist.type == HlsPlaylist.TYPE_MEDIA) { if (playlist.type == HlsPlaylist.TYPE_MEDIA) {
...@@ -526,7 +525,7 @@ public class HlsChunkSource { ...@@ -526,7 +525,7 @@ public class HlsChunkSource {
return true; return true;
} }
private class MediaPlaylistChunk extends BitArrayChunk { private class MediaPlaylistChunk extends DataChunk {
@SuppressWarnings("hiding") @SuppressWarnings("hiding")
/* package */ final int variantIndex; /* package */ final int variantIndex;
...@@ -535,37 +534,38 @@ public class HlsChunkSource { ...@@ -535,37 +534,38 @@ public class HlsChunkSource {
public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec, public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec,
Uri playlistBaseUri) { Uri playlistBaseUri) {
super(dataSource, dataSpec, bitArray); super(dataSource, dataSpec, scratchSpace);
this.variantIndex = variantIndex; this.variantIndex = variantIndex;
this.playlistBaseUri = playlistBaseUri; this.playlistBaseUri = playlistBaseUri;
} }
@Override @Override
protected void consume(BitArray data) throws IOException { protected void consume(byte[] data, int limit) throws IOException {
HlsPlaylist playlist = playlistParser.parse( HlsPlaylist playlist = playlistParser.parse(new ByteArrayInputStream(data, 0, limit),
new ByteArrayInputStream(data.getData(), 0, data.bytesLeft()), null, null, null, null, playlistBaseUri);
playlistBaseUri);
Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MEDIA); Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MEDIA);
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
setMediaPlaylist(variantIndex, mediaPlaylist); setMediaPlaylist(variantIndex, mediaPlaylist);
// Recycle the allocation.
scratchSpace = data;
} }
} }
private class EncryptionKeyChunk extends BitArrayChunk { private class EncryptionKeyChunk extends DataChunk {
private final String iv; private final String iv;
public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) { public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) {
super(dataSource, dataSpec, bitArray); super(dataSource, dataSpec, scratchSpace);
this.iv = iv; this.iv = iv;
} }
@Override @Override
protected void consume(BitArray data) throws IOException { protected void consume(byte[] data, int limit) throws IOException {
byte[] secretKey = new byte[data.bytesLeft()]; initEncryptedDataSource(dataSpec.uri, iv, Arrays.copyOf(data, limit));
data.readBytes(secretKey, 0, secretKey.length); // Recycle the allocation.
initEncryptedDataSource(dataSpec.uri, iv, secretKey); scratchSpace = data;
} }
} }
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer.hls; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; 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.text.eia608.Eia608Parser;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
...@@ -824,43 +825,49 @@ public final class TsExtractor { ...@@ -824,43 +825,49 @@ public final class TsExtractor {
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
private int readOneH264Frame(BitArray pesBuffer, boolean remainderOnly) { private int readOneH264Frame(BitArray pesBuffer, boolean remainderOnly) {
int offset = remainderOnly ? 0 : 3; byte[] pesData = pesBuffer.getData();
int audStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_AUD, offset); 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) { if (currentSample != null) {
int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset); int idrOffset = Mp4Util.findNalUnit(pesData, searchOffset, pesLimit, NAL_UNIT_TYPE_IDR);
if (idrStart < audStart) { if (idrOffset < audOffset) {
currentSample.isKeyframe = true; currentSample.isKeyframe = true;
} }
addToSample(currentSample, pesBuffer, audStart); addToSample(currentSample, pesBuffer, bytesToNextAud);
} else { } else {
pesBuffer.skipBytes(audStart); pesBuffer.skipBytes(bytesToNextAud);
} }
return audStart; return bytesToNextAud;
} }
private void parseMediaFormat(Sample sample) { 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. // Locate the SPS and PPS units.
int spsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_SPS, 0); int spsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_SPS);
int ppsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_PPS, 0); int ppsOffset = Mp4Util.findNalUnit(sampleData, 0, sampleSize, NAL_UNIT_TYPE_PPS);
if (spsOffset == bitArray.bytesLeft() || ppsOffset == bitArray.bytesLeft()) { if (spsOffset == sampleSize || ppsOffset == sampleSize) {
return; return;
} }
int spsLength = bitArray.findNextNalUnit(-1, spsOffset + 3) - spsOffset; // Determine the length of the units, and copy them to build the initialization data.
int ppsLength = bitArray.findNextNalUnit(-1, ppsOffset + 3) - ppsOffset; int spsLength = Mp4Util.findNextNalUnit(sampleData, spsOffset + 3, sampleSize) - spsOffset;
int ppsLength = Mp4Util.findNextNalUnit(sampleData, ppsOffset + 3, sampleSize) - ppsOffset;
byte[] spsData = new byte[spsLength]; byte[] spsData = new byte[spsLength];
byte[] ppsData = new byte[ppsLength]; byte[] ppsData = new byte[ppsLength];
System.arraycopy(bitArray.getData(), spsOffset, spsData, 0, spsLength); System.arraycopy(sampleData, spsOffset, spsData, 0, spsLength);
System.arraycopy(bitArray.getData(), ppsOffset, ppsData, 0, ppsLength); System.arraycopy(sampleData, ppsOffset, ppsData, 0, ppsLength);
List<byte[]> initializationData = new ArrayList<byte[]>(); List<byte[]> initializationData = new ArrayList<byte[]>();
initializationData.add(spsData); initializationData.add(spsData);
initializationData.add(ppsData); initializationData.add(ppsData);
// Unescape the SPS unit. // Unescape and then parse the SPS unit.
byte[] unescapedSps = unescapeStream(spsData, 0, spsLength); 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. // Skip the NAL header.
bitArray.skipBytes(4); bitArray.skipBytes(4);
int profileIdc = bitArray.readBits(8); int profileIdc = bitArray.readBits(8);
...@@ -1033,14 +1040,15 @@ public final class TsExtractor { ...@@ -1033,14 +1040,15 @@ public final class TsExtractor {
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
public void read(byte[] data, int size, long pesTimeUs) { public void read(byte[] data, int length, long pesTimeUs) {
seiBuffer.reset(data, size); seiBuffer.reset(data, length);
while (seiBuffer.bytesLeft() > 0) { while (seiBuffer.bytesLeft() > 0) {
int seiStart = seiBuffer.findNextNalUnit(NAL_UNIT_TYPE_SEI, 0); int currentOffset = seiBuffer.getByteOffset();
if (seiStart == seiBuffer.bytesLeft()) { int seiOffset = Mp4Util.findNalUnit(data, currentOffset, length, NAL_UNIT_TYPE_SEI);
if (seiOffset == length) {
return; return;
} }
seiBuffer.skipBytes(seiStart + 4); seiBuffer.skipBytes(seiOffset + 4 - currentOffset);
int ccDataSize = Eia608Parser.parseHeader(seiBuffer); int ccDataSize = Eia608Parser.parseHeader(seiBuffer);
if (ccDataSize > 0) { if (ccDataSize > 0) {
addSample(Sample.TYPE_MISC, seiBuffer, ccDataSize, pesTimeUs, true); addSample(Sample.TYPE_MISC, seiBuffer, ccDataSize, pesTimeUs, true);
...@@ -1105,7 +1113,7 @@ public final class TsExtractor { ...@@ -1105,7 +1113,7 @@ public final class TsExtractor {
return false; return false;
} }
int offsetToSyncWord = adtsBuffer.findNextAdtsSyncWord(); int offsetToSyncWord = findOffsetToSyncWord();
adtsBuffer.skipBytes(offsetToSyncWord); adtsBuffer.skipBytes(offsetToSyncWord);
int adtsStartOffset = adtsBuffer.getByteOffset(); int adtsStartOffset = adtsBuffer.getByteOffset();
...@@ -1168,6 +1176,25 @@ public final class TsExtractor { ...@@ -1168,6 +1176,25 @@ public final class TsExtractor {
adtsBuffer.reset(); 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;
}
} }
/** /**
......
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
package com.google.android.exoplayer.metadata; package com.google.android.exoplayer.metadata;
import com.google.android.exoplayer.ParserException; 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.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.Collections; import java.util.Collections;
...@@ -37,30 +37,28 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> { ...@@ -37,30 +37,28 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
@Override @Override
public Map<String, Object> parse(byte[] data, int size) public Map<String, Object> parse(byte[] data, int size)
throws UnsupportedEncodingException, ParserException { throws UnsupportedEncodingException, ParserException {
BitArray id3Buffer = new BitArray(data, size);
int id3Size = parseId3Header(id3Buffer);
Map<String, Object> metadata = new HashMap<String, Object>(); Map<String, Object> metadata = new HashMap<String, Object>();
ParsableByteArray id3Data = new ParsableByteArray(data);
int id3Size = parseId3Header(id3Data);
while (id3Size > 0) { while (id3Size > 0) {
int frameId0 = id3Buffer.readUnsignedByte(); int frameId0 = id3Data.readUnsignedByte();
int frameId1 = id3Buffer.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte();
int frameId2 = id3Buffer.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte();
int frameId3 = id3Buffer.readUnsignedByte(); int frameId3 = id3Data.readUnsignedByte();
int frameSize = id3Data.readSynchSafeInt();
int frameSize = id3Buffer.readSynchSafeInt();
if (frameSize <= 1) { if (frameSize <= 1) {
break; break;
} }
id3Buffer.skipBytes(2); // Skip frame flags. // Skip frame flags.
id3Data.skip(2);
// Check Frame ID == TXXX. // Check Frame ID == TXXX.
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') {
int encoding = id3Buffer.readUnsignedByte(); int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding); String charset = getCharsetName(encoding);
byte[] frame = new byte[frameSize - 1]; 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); int firstZeroIndex = indexOf(frame, 0, (byte) 0);
String description = new String(frame, 0, firstZeroIndex, charset); String description = new String(frame, 0, firstZeroIndex, charset);
...@@ -72,7 +70,7 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> { ...@@ -72,7 +70,7 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
} else { } else {
String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3); String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
byte[] frame = new byte[frameSize]; byte[] frame = new byte[frameSize];
id3Buffer.readBytes(frame, 0, frameSize); id3Data.readBytes(frame, 0, frameSize);
metadata.put(type, frame); metadata.put(type, frame);
} }
...@@ -101,12 +99,13 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> { ...@@ -101,12 +99,13 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
} }
/** /**
* Parses ID3 header. * Parses an ID3 header.
* @param id3Buffer A {@link BitArray} with raw ID3 data. *
* @return The size of data that contains ID3 frames without header and footer. * @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". * @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 id1 = id3Buffer.readUnsignedByte();
int id2 = id3Buffer.readUnsignedByte(); int id2 = id3Buffer.readUnsignedByte();
int id3 = id3Buffer.readUnsignedByte(); int id3 = id3Buffer.readUnsignedByte();
...@@ -114,7 +113,7 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> { ...@@ -114,7 +113,7 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
throw new ParserException(String.format( throw new ParserException(String.format(
"Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); "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 flags = id3Buffer.readUnsignedByte();
int id3Size = id3Buffer.readSynchSafeInt(); int id3Size = id3Buffer.readSynchSafeInt();
...@@ -123,7 +122,7 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> { ...@@ -123,7 +122,7 @@ public class Id3Parser implements MetadataParser<Map<String, Object>> {
if ((flags & 0x2) != 0) { if ((flags & 0x2) != 0) {
int extendedHeaderSize = id3Buffer.readSynchSafeInt(); int extendedHeaderSize = id3Buffer.readSynchSafeInt();
if (extendedHeaderSize > 4) { if (extendedHeaderSize > 4) {
id3Buffer.skipBytes(extendedHeaderSize - 4); id3Buffer.skip(extendedHeaderSize - 4);
} }
id3Size -= extendedHeaderSize; id3Size -= extendedHeaderSize;
} }
......
...@@ -99,4 +99,37 @@ public final class Mp4Util { ...@@ -99,4 +99,37 @@ public final class Mp4Util {
return CodecSpecificDataUtil.buildNalUnit(atom.data, offset, length); 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}.
* <p>
* 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;
}
} }
...@@ -272,6 +272,13 @@ public final class BitArray { ...@@ -272,6 +272,13 @@ public final class BitArray {
} }
/** /**
* @return The limit of the data, specified in whole bytes.
*/
public int limit() {
return limit;
}
/**
* @return Whether or not there is any data available. * @return Whether or not there is any data available.
*/ */
public boolean isEmpty() { public boolean isEmpty() {
...@@ -305,57 +312,4 @@ public final class BitArray { ...@@ -305,57 +312,4 @@ public final class BitArray {
return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0); 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;
}
} }
...@@ -27,11 +27,20 @@ public final class ParsableByteArray { ...@@ -27,11 +27,20 @@ public final class ParsableByteArray {
private int position; private int position;
/** Creates a new parsable array with {@code length} bytes. */ /** Creates a new instance with {@code length} bytes. */
public ParsableByteArray(int length) { public ParsableByteArray(int length) {
this.data = new byte[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. */ /** Returns the number of bytes in the array. */
public int length() { public int length() {
return data.length; return data.length;
...@@ -128,6 +137,22 @@ public final class ParsableByteArray { ...@@ -128,6 +137,22 @@ public final class ParsableByteArray {
} }
/** /**
* Reads a Synchsafe integer.
* <p>
* 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. * Reads the next four bytes as an unsigned integer into an integer, if the top bit is a zero.
* *
* @throws IllegalArgumentException Thrown if the top bit of the input data is set. * @throws IllegalArgumentException Thrown if the top bit of the input data is set.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment