Commit 8f0d576f by ojw28

Merge pull request #293 from google/dev

dev -> dev-webm-vp9-opus
parents ccac9fad 40411269
Showing with 1291 additions and 577 deletions
...@@ -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"/>
......
...@@ -92,9 +92,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -92,9 +92,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
private DemoPlayer player; private DemoPlayer player;
private boolean playerNeedsPrepare; private boolean playerNeedsPrepare;
private boolean autoPlay = true;
private long playerPosition; private long playerPosition;
private boolean enableBackgroundAudio = false; private boolean enableBackgroundAudio;
private Uri contentUri; private Uri contentUri;
private int contentType; private int contentType;
...@@ -166,10 +165,10 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -166,10 +165,10 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
if (!enableBackgroundAudio) { if (!enableBackgroundAudio) {
releasePlayer(); releasePlayer();
} else { } else {
player.blockingClearSurface(); player.setBackgrounded(true);
} }
audioCapabilitiesReceiver.unregister(); audioCapabilitiesReceiver.unregister();
shutterView.setVisibility(View.VISIBLE);
} }
@Override @Override
...@@ -183,7 +182,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -183,7 +182,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
@Override @Override
public void onClick(View view) { public void onClick(View view) {
if (view == retryButton) { if (view == retryButton) {
autoPlay = true;
preparePlayer(); preparePlayer();
} }
} }
...@@ -192,11 +190,14 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -192,11 +190,14 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
@Override @Override
public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
this.audioCapabilities = audioCapabilities; boolean audioCapabilitiesChanged = !audioCapabilities.equals(this.audioCapabilities);
releasePlayer(); if (player == null || audioCapabilitiesChanged) {
this.audioCapabilities = audioCapabilities;
autoPlay = true; releasePlayer();
preparePlayer(); preparePlayer();
} else if (player != null) {
player.setBackgrounded(false);
}
} }
// Internal methods // Internal methods
...@@ -239,15 +240,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -239,15 +240,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
updateButtonVisibilities(); updateButtonVisibilities();
} }
player.setSurface(surfaceView.getHolder().getSurface()); player.setSurface(surfaceView.getHolder().getSurface());
maybeStartPlayback(); player.setPlayWhenReady(true);
}
private void maybeStartPlayback() {
if (autoPlay && (player.getSurface().isValid()
|| player.getSelectedTrackIndex(DemoPlayer.TYPE_VIDEO) == DemoPlayer.DISABLED_TRACK)) {
player.setPlayWhenReady(true);
autoPlay = false;
}
} }
private void releasePlayer() { private void releasePlayer() {
...@@ -468,7 +461,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, ...@@ -468,7 +461,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
public void surfaceCreated(SurfaceHolder holder) { public void surfaceCreated(SurfaceHolder holder) {
if (player != null) { if (player != null) {
player.setSurface(holder.getSurface()); player.setSurface(holder.getSurface());
maybeStartPlayback();
} }
} }
......
...@@ -179,10 +179,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -179,10 +179,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
private Surface surface; private Surface surface;
private InternalRendererBuilderCallback builderCallback; private InternalRendererBuilderCallback builderCallback;
private TrackRenderer videoRenderer; private TrackRenderer videoRenderer;
private int videoTrackToRestore;
private MultiTrackChunkSource[] multiTrackSources; private MultiTrackChunkSource[] multiTrackSources;
private String[][] trackNames; private String[][] trackNames;
private int[] selectedTracks; private int[] selectedTracks;
private boolean backgrounded;
private TextListener textListener; private TextListener textListener;
private Id3MetadataListener id3MetadataListener; private Id3MetadataListener id3MetadataListener;
...@@ -233,7 +235,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -233,7 +235,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
public void setSurface(Surface surface) { public void setSurface(Surface surface) {
this.surface = surface; this.surface = surface;
pushSurfaceAndVideoTrack(false); pushSurface(false);
} }
public Surface getSurface() { public Surface getSurface() {
...@@ -242,7 +244,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -242,7 +244,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
public void blockingClearSurface() { public void blockingClearSurface() {
surface = null; surface = null;
pushSurfaceAndVideoTrack(true); pushSurface(true);
} }
public String[] getTracks(int type) { public String[] getTracks(int type) {
...@@ -258,13 +260,23 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -258,13 +260,23 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
return; return;
} }
selectedTracks[type] = index; selectedTracks[type] = index;
if (type == TYPE_VIDEO) { pushTrackSelection(type, true);
pushSurfaceAndVideoTrack(false); 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 { } else {
pushTrackSelection(type, true); selectTrack(TYPE_VIDEO, videoTrackToRestore);
if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) {
textListener.onText(null);
}
} }
} }
...@@ -307,7 +319,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -307,7 +319,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
this.trackNames = trackNames; this.trackNames = trackNames;
this.multiTrackSources = multiTrackSources; this.multiTrackSources = multiTrackSources;
rendererBuildingState = RENDERER_BUILDING_STATE_BUILT; rendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
pushSurfaceAndVideoTrack(false); pushSurface(false);
pushTrackSelection(TYPE_VIDEO, true);
pushTrackSelection(TYPE_AUDIO, true); pushTrackSelection(TYPE_AUDIO, true);
pushTrackSelection(TYPE_TEXT, true); pushTrackSelection(TYPE_TEXT, true);
player.prepare(renderers); player.prepare(renderers);
...@@ -550,7 +563,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -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) { if (rendererBuildingState != RENDERER_BUILDING_STATE_BUILT) {
return; return;
} }
...@@ -562,7 +575,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -562,7 +575,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
player.sendMessage( player.sendMessage(
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
} }
pushTrackSelection(TYPE_VIDEO, surface != null && surface.isValid());
} }
private void pushTrackSelection(int type, boolean allowRendererEnable) { private void pushTrackSelection(int type, boolean allowRendererEnable) {
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer; package com.google.android.exoplayer;
import android.media.MediaExtractor;
/** /**
* Defines constants that are generally useful throughout the library. * Defines constants that are generally useful throughout the library.
*/ */
...@@ -40,6 +42,12 @@ public final class C { ...@@ -40,6 +42,12 @@ public final class C {
*/ */
public static final String UTF8_NAME = "UTF-8"; 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() {} private C() {}
} }
...@@ -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}
......
...@@ -440,17 +440,14 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -440,17 +440,14 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
checkForDiscontinuity(); checkForDiscontinuity();
if (format == null) { if (format == null) {
readFormat(); readFormat();
} else if (codec == null && !shouldInitCodec() && getState() == TrackRenderer.STATE_STARTED) { }
discardSamples(positionUs); if (codec == null && shouldInitCodec()) {
} else { maybeInitCodec();
if (codec == null && shouldInitCodec()) { }
maybeInitCodec(); if (codec != null) {
} while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
if (codec != null) { if (feedInputBuffer(true)) {
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} while (feedInputBuffer(false)) {}
if (feedInputBuffer(true)) {
while (feedInputBuffer(false)) {}
}
} }
} }
codecCounters.ensureUpdated(); codecCounters.ensureUpdated();
...@@ -466,21 +463,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -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 { private void checkForDiscontinuity() throws IOException, ExoPlaybackException {
if (codec == null) { if (codec == null) {
return; return;
...@@ -590,7 +572,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -590,7 +572,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
if (waitingForFirstSyncFrame) { if (waitingForFirstSyncFrame) {
// TODO: Find out if it's possible to supply samples prior to the first sync // TODO: Find out if it's possible to supply samples prior to the first sync
// frame for HE-AAC. // frame for HE-AAC.
if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) == 0) { if ((sampleHolder.flags & C.SAMPLE_FLAG_SYNC) == 0) {
sampleHolder.data.clear(); sampleHolder.data.clear();
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
// The buffer we just cleared contained reconfiguration data. We need to re-write this // The buffer we just cleared contained reconfiguration data. We need to re-write this
......
...@@ -353,7 +353,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -353,7 +353,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
@Override @Override
protected boolean shouldInitCodec() { protected boolean shouldInitCodec() {
return super.shouldInitCodec() && surface != null; return super.shouldInitCodec() && surface != null && surface.isValid();
} }
// Override configureCodec to provide the surface. // Override configureCodec to provide the surface.
......
...@@ -479,7 +479,9 @@ public final class AudioTrack { ...@@ -479,7 +479,9 @@ public final class AudioTrack {
/** Returns whether enough data has been supplied via {@link #handleBuffer} to begin playback. */ /** Returns whether enough data has been supplied via {@link #handleBuffer} to begin playback. */
public boolean hasEnoughDataToBeginPlayback() { 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. */ /** Sets the playback volume. */
......
...@@ -638,7 +638,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -638,7 +638,7 @@ public final class FragmentedMp4Extractor implements Extractor {
} }
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
out.initEncryptionData(senc.length() - senc.getPosition()); out.initEncryptionData(senc.bytesLeft());
out.fillEncryptionData(senc); out.fillEncryptionData(senc);
} }
...@@ -696,7 +696,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -696,7 +696,7 @@ public final class FragmentedMp4Extractor implements Extractor {
offset += sizes[i]; 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) { private int readEncryptionData(NonBlockingInputStream inputStream) {
...@@ -762,7 +762,6 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -762,7 +762,6 @@ public final class FragmentedMp4Extractor implements Extractor {
return 0; return 0;
} }
@SuppressLint("InlinedApi")
private int readSample(NonBlockingInputStream inputStream, int sampleSize, SampleHolder out) { private int readSample(NonBlockingInputStream inputStream, int sampleSize, SampleHolder out) {
if (out == null) { if (out == null) {
return RESULT_NEED_SAMPLE_HOLDER; return RESULT_NEED_SAMPLE_HOLDER;
...@@ -770,7 +769,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -770,7 +769,7 @@ public final class FragmentedMp4Extractor implements Extractor {
out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L; out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L;
out.flags = 0; out.flags = 0;
if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) { if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) {
out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC; out.flags |= C.SAMPLE_FLAG_SYNC;
lastSyncSampleIndex = sampleIndex; lastSyncSampleIndex = sampleIndex;
} }
if (out.data == null || out.data.capacity() < sampleSize) { if (out.data == null || out.data.capacity() < sampleSize) {
......
...@@ -113,7 +113,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -113,7 +113,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
* @param length The length in bytes of the encryption data. * @param length The length in bytes of the encryption data.
*/ */
public void initEncryptionData(int length) { public void initEncryptionData(int length) {
if (sampleEncryptionData == null || sampleEncryptionData.length() < length) { if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) {
sampleEncryptionData = new ParsableByteArray(length); sampleEncryptionData = new ParsableByteArray(length);
} }
sampleEncryptionDataLength = length; sampleEncryptionDataLength = length;
......
...@@ -25,9 +25,6 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream; ...@@ -25,9 +25,6 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.LongArray; import com.google.android.exoplayer.util.LongArray;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.TargetApi;
import android.media.MediaExtractor;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
...@@ -42,7 +39,6 @@ import java.util.concurrent.TimeUnit; ...@@ -42,7 +39,6 @@ import java.util.concurrent.TimeUnit;
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>. * Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>. * More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
*/ */
@TargetApi(16)
public final class WebmExtractor implements Extractor { public final class WebmExtractor implements Extractor {
private static final String DOC_TYPE_WEBM = "webm"; private static final String DOC_TYPE_WEBM = "webm";
...@@ -412,7 +408,7 @@ public final class WebmExtractor implements Extractor { ...@@ -412,7 +408,7 @@ public final class WebmExtractor implements Extractor {
case LACING_NONE: case LACING_NONE:
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes; long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs; simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; sampleHolder.flags = keyframe ? C.SAMPLE_FLAG_SYNC : 0;
sampleHolder.decodeOnly = invisible; sampleHolder.decodeOnly = invisible;
sampleHolder.timeUs = clusterTimecodeUs + timecodeUs; sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead()); sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
......
...@@ -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);
}
}
} }
...@@ -17,14 +17,14 @@ package com.google.android.exoplayer.hls; ...@@ -17,14 +17,14 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.hls.TsExtractor.SamplePool; import com.google.android.exoplayer.hls.parser.TsExtractor;
import com.google.android.exoplayer.upstream.Aes128DataSource; import com.google.android.exoplayer.upstream.Aes128DataSource;
import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException;
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 +35,7 @@ import java.io.ByteArrayInputStream; ...@@ -35,6 +35,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;
...@@ -82,6 +83,11 @@ public class HlsChunkSource { ...@@ -82,6 +83,11 @@ public class HlsChunkSource {
public static final int ADAPTIVE_MODE_ABRUPT = 3; 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. * The default target buffer duration in milliseconds.
*/ */
public static final long DEFAULT_TARGET_BUFFER_DURATION_MS = 40000; public static final long DEFAULT_TARGET_BUFFER_DURATION_MS = 40000;
...@@ -101,20 +107,21 @@ public class HlsChunkSource { ...@@ -101,20 +107,21 @@ public class HlsChunkSource {
private static final String TAG = "HlsChunkSource"; private static final String TAG = "HlsChunkSource";
private static final float BANDWIDTH_FRACTION = 0.8f; private static final float BANDWIDTH_FRACTION = 0.8f;
private final SamplePool samplePool = new TsExtractor.SamplePool(); private final BufferPool bufferPool;
private final DataSource upstreamDataSource; private final DataSource upstreamDataSource;
private final HlsPlaylistParser playlistParser; private final HlsPlaylistParser playlistParser;
private final Variant[] enabledVariants; private final Variant[] enabledVariants;
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;
private final int maxHeight; private final int maxHeight;
private final int targetBufferSize;
private final long targetBufferDurationUs; private final long targetBufferDurationUs;
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;
...@@ -130,8 +137,8 @@ public class HlsChunkSource { ...@@ -130,8 +137,8 @@ public class HlsChunkSource {
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) { BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) {
this(dataSource, playlistUrl, playlist, bandwidthMeter, variantIndices, adaptiveMode, this(dataSource, playlistUrl, playlist, bandwidthMeter, variantIndices, adaptiveMode,
DEFAULT_TARGET_BUFFER_DURATION_MS, DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, DEFAULT_TARGET_BUFFER_SIZE, DEFAULT_TARGET_BUFFER_DURATION_MS,
DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS); DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS);
} }
/** /**
...@@ -144,9 +151,10 @@ public class HlsChunkSource { ...@@ -144,9 +151,10 @@ public class HlsChunkSource {
* @param adaptiveMode The mode for switching from one variant to another. One of * @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_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and
* {@link #ADAPTIVE_MODE_SPLICE}. * {@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 * @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 * playback position. The buffer will not be filled more than one chunk beyond this position.
* that will be consumed.
* @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered * @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered
* for a switch to a higher quality variant to be considered. * for a switch to a higher quality variant to be considered.
* @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered * @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered
...@@ -154,17 +162,18 @@ public class HlsChunkSource { ...@@ -154,17 +162,18 @@ public class HlsChunkSource {
*/ */
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode, BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode,
long targetBufferDurationMs, long minBufferDurationToSwitchUpMs, int targetBufferSize, long targetBufferDurationMs, long minBufferDurationToSwitchUpMs,
long maxBufferDurationToSwitchDownMs) { long maxBufferDurationToSwitchDownMs) {
this.upstreamDataSource = dataSource; this.upstreamDataSource = dataSource;
this.bandwidthMeter = bandwidthMeter; this.bandwidthMeter = bandwidthMeter;
this.adaptiveMode = adaptiveMode; this.adaptiveMode = adaptiveMode;
this.targetBufferSize = targetBufferSize;
targetBufferDurationUs = targetBufferDurationMs * 1000; targetBufferDurationUs = targetBufferDurationMs * 1000;
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();
bufferPool = new BufferPool(256 * 1024);
if (playlist.type == HlsPlaylist.TYPE_MEDIA) { if (playlist.type == HlsPlaylist.TYPE_MEDIA) {
enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)}; enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)};
...@@ -225,8 +234,9 @@ public class HlsChunkSource { ...@@ -225,8 +234,9 @@ public class HlsChunkSource {
public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs, public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs,
long playbackPositionUs) { long playbackPositionUs) {
if (previousTsChunk != null && (previousTsChunk.isLastChunk if (previousTsChunk != null && (previousTsChunk.isLastChunk
|| previousTsChunk.endTimeUs - playbackPositionUs >= targetBufferDurationUs)) { || previousTsChunk.endTimeUs - playbackPositionUs >= targetBufferDurationUs)
// We're either finished, or we have the target amount of data buffered. || bufferPool.getAllocatedSize() >= targetBufferSize) {
// We're either finished, or we have the target amount of data or time buffered.
return null; return null;
} }
...@@ -324,7 +334,7 @@ public class HlsChunkSource { ...@@ -324,7 +334,7 @@ public class HlsChunkSource {
// Configure the extractor that will read the chunk. // Configure the extractor that will read the chunk.
TsExtractor extractor; TsExtractor extractor;
if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) { if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) {
extractor = new TsExtractor(startTimeUs, samplePool, switchingVariantSpliced); extractor = new TsExtractor(startTimeUs, switchingVariantSpliced, bufferPool);
} else { } else {
extractor = previousTsChunk.extractor; extractor = previousTsChunk.extractor;
} }
...@@ -526,7 +536,7 @@ public class HlsChunkSource { ...@@ -526,7 +536,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 +545,38 @@ public class HlsChunkSource { ...@@ -535,37 +545,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;
} }
} }
......
...@@ -21,6 +21,7 @@ import com.google.android.exoplayer.SampleHolder; ...@@ -21,6 +21,7 @@ import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer; 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;
import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.hls; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
......
/*
* 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.upstream.BufferPool;
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 lastByteWasFF;
private boolean hasCrc;
// Parsed from the header.
private long frameDurationUs;
private int sampleSize;
// Used when reading the samples.
private long timeUs;
public AdtsReader(BufferPool bufferPool) {
super(bufferPool);
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();
startSample(timeUs);
bytesRead = 0;
state = STATE_READING_SAMPLE;
}
break;
case STATE_READING_SAMPLE:
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
appendData(data, bytesToRead);
bytesRead += bytesToRead;
if (bytesRead == sampleSize) {
commitSample(true);
timeUs += frameDurationUs;
bytesRead = 0;
state = STATE_FINDING_SYNC;
}
break;
}
}
}
@Override
public void packetFinished() {
// Do nothing.
}
/**
* 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.limit();
for (int i = startOffset; i < endOffset; i++) {
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);
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<Integer, Integer> 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;
}
}
}
/*
* 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.upstream.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
* Parses ID3 data and extracts individual text information frames.
*/
/* package */ class Id3Reader extends PesPayloadReader {
public Id3Reader(BufferPool bufferPool) {
super(bufferPool);
setMediaFormat(MediaFormat.createId3Format());
}
@Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
if (startOfPacket) {
startSample(pesTimeUs);
}
if (writingSample()) {
appendData(data, data.bytesLeft());
}
}
@Override
public void packetFinished() {
commitSample(true);
}
}
/*
* 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.upstream.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
* Extracts individual samples from continuous byte stream, preserving original order.
*/
/* package */ abstract class PesPayloadReader extends SampleQueue {
protected PesPayloadReader(BufferPool bufferPool) {
super(bufferPool);
}
/**
* 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();
}
/*
* 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.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
* 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 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;
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 void release() {
rollingBuffer.release();
}
// Called by the consuming thread.
public long getLargestParsedTimestampUs() {
return largestParsedTimestampUs;
}
public boolean hasMediaFormat() {
return mediaFormat != null;
}
public MediaFormat getMediaFormat() {
return mediaFormat;
}
public boolean isEmpty() {
return !advanceToEligibleSample();
}
/**
* Removes the next sample from the head of the queue, writing it into the provided holder.
* <p>
* The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples
* queued prior to the first keyframe are discarded.
*
* @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise.
*/
public boolean getSample(SampleHolder holder) {
boolean foundEligibleSample = advanceToEligibleSample();
if (!foundEligibleSample) {
return false;
}
// Write the sample into the holder.
rollingBuffer.readSample(holder);
needKeyframe = false;
lastReadTimeUs = holder.timeUs;
return true;
}
/**
* 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) {
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;
}
/**
* 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;
if (rollingBuffer.peekSample(sampleInfoHolder)) {
firstPossibleSpliceTime = sampleInfoHolder.timeUs;
} else {
firstPossibleSpliceTime = lastReadTimeUs + 1;
}
RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer;
while (nextRollingBuffer.peekSample(sampleInfoHolder)
&& (sampleInfoHolder.timeUs < firstPossibleSpliceTime
|| (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();
}
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 = sampleInfoHolder.timeUs;
return true;
}
return false;
}
/**
* 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.
*/
private boolean advanceToEligibleSample() {
boolean haveNext = rollingBuffer.peekSample(sampleInfoHolder);
if (needKeyframe) {
while (haveNext && (sampleInfoHolder.flags & C.SAMPLE_FLAG_SYNC) == 0) {
rollingBuffer.skipSample();
haveNext = rollingBuffer.peekSample(sampleInfoHolder);
}
}
if (!haveNext) {
return false;
}
if (spliceOutTimeUs != Long.MIN_VALUE && sampleInfoHolder.timeUs >= spliceOutTimeUs) {
return false;
}
return true;
}
// Called by the loading thread.
protected boolean writingSample() {
return writingSample;
}
protected void setMediaFormat(MediaFormat mediaFormat) {
this.mediaFormat = mediaFormat;
}
protected void startSample(long sampleTimeUs) {
startSample(sampleTimeUs, 0);
}
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) {
commitSample(isKeyframe, 0);
}
protected void commitSample(boolean isKeyframe, int offset) {
rollingBuffer.commitSample(isKeyframe, offset);
writingSample = false;
}
}
/*
* 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.text.eia608.Eia608Parser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
* 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 {
private final ParsableByteArray seiBuffer;
public SeiReader(BufferPool bufferPool) {
super(bufferPool);
setMediaFormat(MediaFormat.createEia608Format());
seiBuffer = new ParsableByteArray();
}
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(pesTimeUs);
appendData(seiBuffer, ccDataSize);
commitSample(true);
}
}
}
...@@ -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, size);
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;
} }
......
...@@ -24,8 +24,6 @@ import com.google.android.exoplayer.util.MimeTypes; ...@@ -24,8 +24,6 @@ import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.media.MediaExtractor;
import android.util.Pair; import android.util.Pair;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -37,8 +35,8 @@ public final class CommonMp4AtomParsers { ...@@ -37,8 +35,8 @@ public final class CommonMp4AtomParsers {
/** Channel counts for AC-3 audio, indexed by acmod. (See ETSI TS 102 366.) */ /** 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}; 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.) */ /** Nominal bitrates 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, 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}; 192, 224, 256, 320, 384, 448, 512, 576, 640};
/** /**
...@@ -81,7 +79,6 @@ public final class CommonMp4AtomParsers { ...@@ -81,7 +79,6 @@ public final class CommonMp4AtomParsers {
* @param stblAtom stbl (sample table) atom to parse. * @param stblAtom stbl (sample table) atom to parse.
* @return Sample table described by the stbl atom. * @return Sample table described by the stbl atom.
*/ */
@SuppressLint("InlinedApi")
public static Mp4TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom) { public static Mp4TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom) {
// Array of sample sizes. // Array of sample sizes.
ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data; ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data;
...@@ -174,9 +171,9 @@ public final class CommonMp4AtomParsers { ...@@ -174,9 +171,9 @@ public final class CommonMp4AtomParsers {
timestamps[i] = timestampTimeUnits + timestampOffset; timestamps[i] = timestampTimeUnits + timestampOffset;
// All samples are synchronization samples if the stss is not present. // 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) { if (i == nextSynchronizationSampleIndex) {
flags[i] = MediaExtractor.SAMPLE_FLAG_SYNC; flags[i] = C.SAMPLE_FLAG_SYNC;
remainingSynchronizationSamples--; remainingSynchronizationSamples--;
if (remainingSynchronizationSamples > 0) { if (remainingSynchronizationSamples > 0) {
nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1; nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
...@@ -639,8 +636,8 @@ public final class CommonMp4AtomParsers { ...@@ -639,8 +636,8 @@ public final class CommonMp4AtomParsers {
channelCount++; channelCount++;
} }
// Map bit_rate_code onto a bit-rate in kbit/s. // Map bit_rate_code onto a bitrate in kbit/s.
int bitrate = AC3_BIT_RATES[((nextByte & 0x03) << 3) + (parent.readUnsignedByte() >> 5)]; int bitrate = AC3_BITRATES[((nextByte & 0x03) << 3) + (parent.readUnsignedByte() >> 5)];
return new Ac3Format(channelCount, sampleRate, bitrate); return new Ac3Format(channelCount, sampleRate, bitrate);
} }
......
...@@ -15,11 +15,10 @@ ...@@ -15,11 +15,10 @@
*/ */
package com.google.android.exoplayer.mp4; 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.Assertions;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.media.MediaExtractor;
/** Sample table for a track in an MP4 file. */ /** Sample table for a track in an MP4 file. */
public final class Mp4TrackSampleTable { public final class Mp4TrackSampleTable {
...@@ -59,7 +58,7 @@ public final class Mp4TrackSampleTable { ...@@ -59,7 +58,7 @@ public final class Mp4TrackSampleTable {
public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) { public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) {
int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false); int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false);
for (int i = startIndex; i >= 0; i--) { 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; return i;
} }
} }
...@@ -77,7 +76,7 @@ public final class Mp4TrackSampleTable { ...@@ -77,7 +76,7 @@ public final class Mp4TrackSampleTable {
public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) { public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) {
int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false); int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false);
for (int i = startIndex; i < timestampsUs.length; i++) { 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; return i;
} }
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.mp4; 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.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
...@@ -99,4 +100,155 @@ public final class Mp4Util { ...@@ -99,4 +100,155 @@ public final class Mp4Util {
return CodecSpecificDataUtil.buildNalUnit(atom.data, offset, length); return CodecSpecificDataUtil.buildNalUnit(atom.data, offset, length);
} }
/**
* 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 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) {
return findNalUnit(data, startOffset, endOffset, type, null);
}
/**
* Like {@link #findNalUnit(byte[], int, int, int)}, but supports finding of NAL units across
* array boundaries.
* <p>
* 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.
* <p>
* 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
// 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
&& 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
// only skip forward by one. The loop will skip forward by three, so subtract two here.
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. 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;
}
} }
...@@ -18,7 +18,7 @@ package com.google.android.exoplayer.text.eia608; ...@@ -18,7 +18,7 @@ package com.google.android.exoplayer.text.eia608;
/** /**
* A Closed Caption that contains textual data associated with time indices. * A Closed Caption that contains textual data associated with time indices.
*/ */
/* package */ abstract class ClosedCaption implements Comparable<ClosedCaption> { /* package */ abstract class ClosedCaption {
/** /**
* Identifies closed captions with control characters. * Identifies closed captions with control characters.
...@@ -33,23 +33,9 @@ package com.google.android.exoplayer.text.eia608; ...@@ -33,23 +33,9 @@ package com.google.android.exoplayer.text.eia608;
* The type of the closed caption data. * The type of the closed caption data.
*/ */
public final int type; 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.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;
} }
} }
...@@ -70,8 +70,8 @@ package com.google.android.exoplayer.text.eia608; ...@@ -70,8 +70,8 @@ package com.google.android.exoplayer.text.eia608;
public final byte cc1; public final byte cc1;
public final byte cc2; public final byte cc2;
protected ClosedCaptionCtrl(byte cc1, byte cc2, long timeUs) { protected ClosedCaptionCtrl(byte cc1, byte cc2) {
super(ClosedCaption.TYPE_CTRL, timeUs); super(ClosedCaption.TYPE_CTRL);
this.cc1 = cc1; this.cc1 = cc1;
this.cc2 = cc2; this.cc2 = cc2;
} }
......
/*
* 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<ClosedCaptionList> {
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;
}
}
...@@ -19,8 +19,8 @@ package com.google.android.exoplayer.text.eia608; ...@@ -19,8 +19,8 @@ package com.google.android.exoplayer.text.eia608;
public final String text; public final String text;
public ClosedCaptionText(String text, long timeUs) { public ClosedCaptionText(String text) {
super(ClosedCaption.TYPE_TEXT, timeUs); super(ClosedCaption.TYPE_TEXT);
this.text = text; this.text = text;
} }
......
...@@ -15,10 +15,12 @@ ...@@ -15,10 +15,12 @@
*/ */
package com.google.android.exoplayer.text.eia608; package com.google.android.exoplayer.text.eia608;
import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.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") * Facilitates the extraction and parsing of EIA-608 (a.k.a. "line 21 captions" and "CEA-608")
...@@ -80,28 +82,31 @@ public class Eia608Parser { ...@@ -80,28 +82,31 @@ public class Eia608Parser {
0xFB // 3F: 251 'û' "Latin small letter U with circumflex" 0xFB // 3F: 251 'û' "Latin small letter U with circumflex"
}; };
private final BitArray seiBuffer; private final ParsableBitArray seiBuffer;
private final StringBuilder stringBuilder; private final StringBuilder stringBuilder;
private final ArrayList<ClosedCaption> captions;
/* package */ Eia608Parser() { /* package */ Eia608Parser() {
seiBuffer = new BitArray(); seiBuffer = new ParsableBitArray();
stringBuilder = new StringBuilder(); stringBuilder = new StringBuilder();
captions = new ArrayList<ClosedCaption>();
} }
/* package */ boolean canParse(String mimeType) { /* package */ boolean canParse(String mimeType) {
return mimeType.equals(MimeTypes.APPLICATION_EIA608); return mimeType.equals(MimeTypes.APPLICATION_EIA608);
} }
/* package */ void parse(byte[] data, int size, long timeUs, List<ClosedCaption> out) { /* package */ ClosedCaptionList parse(SampleHolder sampleHolder) {
if (size <= 0) { if (sampleHolder.size <= 0) {
return; return null;
} }
captions.clear();
stringBuilder.setLength(0); stringBuilder.setLength(0);
seiBuffer.reset(data, size); seiBuffer.reset(sampleHolder.data.array());
seiBuffer.skipBits(3); // reserved + process_cc_data_flag + zero_bit seiBuffer.skipBits(3); // reserved + process_cc_data_flag + zero_bit
int ccCount = seiBuffer.readBits(5); int ccCount = seiBuffer.readBits(5);
seiBuffer.skipBytes(1); seiBuffer.skipBits(8);
for (int i = 0; i < ccCount; i++) { for (int i = 0; i < ccCount; i++) {
seiBuffer.skipBits(5); // one_bit + reserved seiBuffer.skipBits(5); // one_bit + reserved
...@@ -134,10 +139,10 @@ public class Eia608Parser { ...@@ -134,10 +139,10 @@ public class Eia608Parser {
// Control character. // Control character.
if (ccData1 < 0x20) { if (ccData1 < 0x20) {
if (stringBuilder.length() > 0) { if (stringBuilder.length() > 0) {
out.add(new ClosedCaptionText(stringBuilder.toString(), timeUs)); captions.add(new ClosedCaptionText(stringBuilder.toString()));
stringBuilder.setLength(0); stringBuilder.setLength(0);
} }
out.add(new ClosedCaptionCtrl(ccData1, ccData2, timeUs)); captions.add(new ClosedCaptionCtrl(ccData1, ccData2));
continue; continue;
} }
...@@ -149,8 +154,16 @@ public class Eia608Parser { ...@@ -149,8 +154,16 @@ public class Eia608Parser {
} }
if (stringBuilder.length() > 0) { 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) { private static char getChar(byte ccData) {
...@@ -170,7 +183,7 @@ public class Eia608Parser { ...@@ -170,7 +183,7 @@ public class Eia608Parser {
* @param seiBuffer The buffer to read from. * @param seiBuffer The buffer to read from.
* @return The size of closed captions data. * @return The size of closed captions data.
*/ */
public static int parseHeader(BitArray seiBuffer) { public static int parseHeader(ParsableByteArray seiBuffer) {
int b = 0; int b = 0;
int payloadType = 0; int payloadType = 0;
...@@ -197,11 +210,11 @@ public class Eia608Parser { ...@@ -197,11 +210,11 @@ public class Eia608Parser {
if (countryCode != COUNTRY_CODE) { if (countryCode != COUNTRY_CODE) {
return 0; return 0;
} }
int providerCode = seiBuffer.readBits(16); int providerCode = seiBuffer.readUnsignedShort();
if (providerCode != PROVIDER_CODE) { if (providerCode != PROVIDER_CODE) {
return 0; return 0;
} }
int userIdentifier = seiBuffer.readBits(32); int userIdentifier = seiBuffer.readInt();
if (userIdentifier != USER_ID) { if (userIdentifier != USER_ID) {
return 0; return 0;
} }
......
...@@ -31,8 +31,7 @@ import android.os.Looper; ...@@ -31,8 +31,7 @@ import android.os.Looper;
import android.os.Message; import android.os.Message;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.TreeSet;
import java.util.List;
/** /**
* A {@link TrackRenderer} for EIA-608 closed captions in a media stream. * A {@link TrackRenderer} for EIA-608 closed captions in a media stream.
...@@ -48,6 +47,8 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -48,6 +47,8 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
// The default number of rows to display in roll-up captions mode. // The default number of rows to display in roll-up captions mode.
private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4; 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 SampleSource source;
private final Eia608Parser eia608Parser; private final Eia608Parser eia608Parser;
...@@ -56,7 +57,7 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -56,7 +57,7 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
private final MediaFormatHolder formatHolder; private final MediaFormatHolder formatHolder;
private final SampleHolder sampleHolder; private final SampleHolder sampleHolder;
private final StringBuilder captionStringBuilder; private final StringBuilder captionStringBuilder;
private final List<ClosedCaption> captionBuffer; private final TreeSet<ClosedCaptionList> pendingCaptionLists;
private int trackIndex; private int trackIndex;
private long currentPositionUs; private long currentPositionUs;
...@@ -85,7 +86,7 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -85,7 +86,7 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
formatHolder = new MediaFormatHolder(); formatHolder = new MediaFormatHolder();
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
captionStringBuilder = new StringBuilder(); captionStringBuilder = new StringBuilder();
captionBuffer = new ArrayList<ClosedCaption>(); pendingCaptionLists = new TreeSet<ClosedCaptionList>();
} }
@Override @Override
...@@ -122,6 +123,7 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -122,6 +123,7 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
private void seekToInternal(long positionUs) { private void seekToInternal(long positionUs) {
currentPositionUs = positionUs; currentPositionUs = positionUs;
inputStreamEnded = false; inputStreamEnded = false;
pendingCaptionLists.clear();
clearPendingSample(); clearPendingSample();
captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
setCaptionMode(CC_MODE_UNKNOWN); setCaptionMode(CC_MODE_UNKNOWN);
...@@ -138,10 +140,17 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -138,10 +140,17 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
throw new ExoPlaybackException(e); 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 { try {
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.END_OF_STREAM) { if (result == SampleSource.SAMPLE_READ) {
maybeParsePendingSample();
} else if (result == SampleSource.END_OF_STREAM) {
inputStreamEnded = true; inputStreamEnded = true;
} }
} catch (IOException e) { } catch (IOException e) {
...@@ -149,17 +158,18 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -149,17 +158,18 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
} }
} }
if (isSamplePending() && sampleHolder.timeUs <= currentPositionUs) { while (!pendingCaptionLists.isEmpty()) {
// Parse the pending sample. if (pendingCaptionLists.first().timeUs > currentPositionUs) {
eia608Parser.parse(sampleHolder.data.array(), sampleHolder.size, sampleHolder.timeUs, // We're too early to render any of the pending caption lists.
captionBuffer); return;
// Consume parsed captions. }
consumeCaptionBuffer(); // Remove and consume the next caption list.
// Update the renderer, unless the sample was marked for decoding only. ClosedCaptionList nextCaptionList = pendingCaptionLists.pollFirst();
if (!sampleHolder.decodeOnly) { consumeCaptionList(nextCaptionList);
// Update the renderer, unless the caption list was marked for decoding only.
if (!nextCaptionList.decodeOnly) {
invokeRenderer(caption); invokeRenderer(caption);
} }
clearPendingSample();
} }
} }
...@@ -221,14 +231,26 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -221,14 +231,26 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
textRenderer.onText(text); textRenderer.onText(text);
} }
private void consumeCaptionBuffer() { private void maybeParsePendingSample() {
int captionBufferSize = captionBuffer.size(); 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) { if (captionBufferSize == 0) {
return; return;
} }
for (int i = 0; i < captionBufferSize; i++) { for (int i = 0; i < captionBufferSize; i++) {
ClosedCaption caption = captionBuffer.get(i); ClosedCaption caption = captionList.captions[i];
if (caption.type == ClosedCaption.TYPE_CTRL) { if (caption.type == ClosedCaption.TYPE_CTRL) {
ClosedCaptionCtrl captionCtrl = (ClosedCaptionCtrl) caption; ClosedCaptionCtrl captionCtrl = (ClosedCaptionCtrl) caption;
if (captionCtrl.isMiscCode()) { if (captionCtrl.isMiscCode()) {
...@@ -240,7 +262,6 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { ...@@ -240,7 +262,6 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
handleText((ClosedCaptionText) caption); handleText((ClosedCaptionText) caption);
} }
} }
captionBuffer.clear();
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
caption = getDisplayCaption(); caption = getDisplayCaption();
......
...@@ -96,13 +96,39 @@ public final class BufferPool implements Allocator { ...@@ -96,13 +96,39 @@ public final class BufferPool implements Allocator {
allocatedBufferCount += requiredBufferCount - firstNewBufferIndex; allocatedBufferCount += requiredBufferCount - firstNewBufferIndex;
for (int i = firstNewBufferIndex; i < requiredBufferCount; i++) { for (int i = firstNewBufferIndex; i < requiredBufferCount; i++) {
// Use a recycled buffer if one is available. Else instantiate a new one. // Use a recycled buffer if one is available. Else instantiate a new one.
buffers[i] = recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount] : buffers[i] = nextBuffer();
new byte[bufferLength];
} }
return buffers; return buffers;
} }
/** /**
* Obtain a single buffer directly from the pool.
* <p>
* When the caller has finished with the buffer, it should be returned to the pool by calling
* {@link #releaseDirect(byte[])}.
*
* @return The allocated buffer.
*/
public synchronized byte[] allocateDirect() {
allocatedBufferCount++;
return nextBuffer();
}
/**
* Return a single buffer to the pool.
*
* @param buffer The buffer being returned.
*/
public synchronized void releaseDirect(byte[] buffer) {
// Weak sanity check that the buffer probably originated from this pool.
Assertions.checkArgument(buffer.length == bufferLength);
allocatedBufferCount--;
ensureRecycledBufferCapacity(recycledBufferCount + 1);
recycledBuffers[recycledBufferCount++] = buffer;
}
/**
* Returns the buffers belonging to an allocation to the pool. * Returns the buffers belonging to an allocation to the pool.
* *
* @param allocation The allocation to return. * @param allocation The allocation to return.
...@@ -112,14 +138,7 @@ public final class BufferPool implements Allocator { ...@@ -112,14 +138,7 @@ public final class BufferPool implements Allocator {
allocatedBufferCount -= buffers.length; allocatedBufferCount -= buffers.length;
int newRecycledBufferCount = recycledBufferCount + buffers.length; int newRecycledBufferCount = recycledBufferCount + buffers.length;
if (recycledBuffers.length < newRecycledBufferCount) { ensureRecycledBufferCapacity(newRecycledBufferCount);
// Expand the capacity of the recycled buffers array.
byte[][] newRecycledBuffers = new byte[newRecycledBufferCount * 2][];
if (recycledBufferCount > 0) {
System.arraycopy(recycledBuffers, 0, newRecycledBuffers, 0, recycledBufferCount);
}
recycledBuffers = newRecycledBuffers;
}
System.arraycopy(buffers, 0, recycledBuffers, recycledBufferCount, buffers.length); System.arraycopy(buffers, 0, recycledBuffers, recycledBufferCount, buffers.length);
recycledBufferCount = newRecycledBufferCount; recycledBufferCount = newRecycledBufferCount;
} }
...@@ -128,6 +147,22 @@ public final class BufferPool implements Allocator { ...@@ -128,6 +147,22 @@ public final class BufferPool implements Allocator {
return (int) ((size + bufferLength - 1) / bufferLength); return (int) ((size + bufferLength - 1) / bufferLength);
} }
private byte[] nextBuffer() {
return recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount]
: new byte[bufferLength];
}
private void ensureRecycledBufferCapacity(int requiredCapacity) {
if (recycledBuffers.length < requiredCapacity) {
// Expand the capacity of the recycled buffers array.
byte[][] newRecycledBuffers = new byte[requiredCapacity * 2][];
if (recycledBufferCount > 0) {
System.arraycopy(recycledBuffers, 0, newRecycledBuffers, 0, recycledBufferCount);
}
recycledBuffers = newRecycledBuffers;
}
}
private class AllocationImpl implements Allocation { private class AllocationImpl implements Allocation {
private byte[][] buffers; private byte[][] buffers;
......
/*
* 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.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 {
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;
}
/**
* 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.
*
* @param data The data to wrap.
* @param limit The limit to set.
*/
public void reset(byte[] data, int limit) {
this.data = data;
this.limit = limit;
byteOffset = 0;
bitOffset = 0;
}
/**
* Gets the backing byte array.
*
* @return The backing byte array.
*/
public byte[] getData() {
return data;
}
/**
* Gets the current byte offset.
*
* @return The current byte offset.
*/
public int getByteOffset() {
return byteOffset;
}
/**
* Sets the current byte offset.
*
* @param byteOffset The byte offset to set.
*/
public void setByteOffset(int byteOffset) {
this.byteOffset = byteOffset;
}
/**
* Appends data from a {@link DataSource}.
*
* @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.
*/
public int append(DataSource dataSource, int length) throws IOException {
expand(length);
int bytesRead = dataSource.read(data, limit, length);
if (bytesRead == -1) {
return -1;
}
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;
}
/**
* Reads a single bit.
*
* @return True if the bit is set. False otherwise.
*/
public boolean readBit() {
return readBits(1) == 1;
}
/**
* Reads up to 32 bits.
*
* @param n The number of bits to read.
* @return An integer whose bottom n bits hold the read data.
*/
public int readBits(int n) {
return (int) readBitsLong(n);
}
/**
* Reads up to 64 bits.
*
* @param n The number of bits to read.
* @return A long whose bottom n bits hold the read data.
*/
public long readBitsLong(int n) {
if (n == 0) {
return 0;
}
long retval = 0;
// While n >= 8, read whole bytes.
while (n >= 8) {
n -= 8;
retval |= (readUnsignedByte() << n);
}
if (n > 0) {
int nextBit = bitOffset + n;
byte writeMask = (byte) (0xFF >> (8 - n));
if (nextBit > 8) {
// Combine bits from current byte and next byte.
retval |= (((getUnsignedByte(byteOffset) << (nextBit - 8)
| (getUnsignedByte(byteOffset + 1) >> (16 - nextBit))) & writeMask));
byteOffset++;
} else {
// Bits to be read only within current byte.
retval |= ((getUnsignedByte(byteOffset) >> (8 - nextBit)) & writeMask);
if (nextBit == 8) {
byteOffset++;
}
}
bitOffset = nextBit % 8;
}
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.
* <p>
* 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 Whether or not there is any data available.
*/
public boolean isEmpty() {
return limit == 0;
}
/**
* Reads an unsigned Exp-Golomb-coded format integer.
*
* @return The value of the parsed Exp-Golomb-coded integer.
*/
public int readUnsignedExpGolombCodedInt() {
return readExpGolombCodeNum();
}
/**
* Reads an signed Exp-Golomb-coded format integer.
*
* @return The value of the parsed Exp-Golomb-coded integer.
*/
public int readSignedExpGolombCodedInt() {
int codeNum = readExpGolombCodeNum();
return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2);
}
private int readExpGolombCodeNum() {
int leadingZeros = 0;
while (!readBit()) {
leadingZeros++;
}
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;
}
}
/*
* 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.util;
/**
* Wraps a byte array, providing methods that allow it to be read as a bitstream.
*/
public final class ParsableBitArray {
private byte[] data;
// 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;
/** Creates a new instance that initially has no backing data. */
public ParsableBitArray() {}
/**
* Creates a new instance that wraps an existing array.
*
* @param data The data to wrap.
*/
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;
byteOffset = 0;
bitOffset = 0;
}
/**
* Gets the backing byte array.
*
* @return The backing byte array.
*/
public byte[] getData() {
return data;
}
/**
* Gets the current bit offset.
*
* @return The current bit offset.
*/
public int getPosition() {
return byteOffset * 8 + bitOffset;
}
/**
* Sets the current bit offset.
*
* @param position The position to set.
*/
public void setPosition(int position) {
byteOffset = position / 8;
bitOffset = position - (byteOffset * 8);
}
/**
* 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;
}
}
/**
* Reads a single bit.
*
* @return True if the bit is set. False otherwise.
*/
public boolean readBit() {
return readBits(1) == 1;
}
/**
* Reads up to 32 bits.
*
* @param n The number of bits to read.
* @return An integer whose bottom n bits hold the read data.
*/
public int readBits(int n) {
return (int) readBitsLong(n);
}
/**
* Reads up to 64 bits.
*
* @param n The number of bits to read.
* @return A long whose bottom n bits hold the read data.
*/
public long readBitsLong(int n) {
if (n == 0) {
return 0;
}
long retval = 0;
// While n >= 8, read whole bytes.
while (n >= 8) {
n -= 8;
retval |= (readUnsignedByte() << n);
}
if (n > 0) {
int nextBit = bitOffset + n;
byte writeMask = (byte) (0xFF >> (8 - n));
if (nextBit > 8) {
// Combine bits from current byte and next byte.
retval |= (((getUnsignedByte(byteOffset) << (nextBit - 8)
| (getUnsignedByte(byteOffset + 1) >> (16 - nextBit))) & writeMask));
byteOffset++;
} else {
// Bits to be read only within current byte.
retval |= ((getUnsignedByte(byteOffset) >> (8 - nextBit)) & writeMask);
if (nextBit == 8) {
byteOffset++;
}
}
bitOffset = nextBit % 8;
}
return retval;
}
/**
* Reads an unsigned Exp-Golomb-coded format integer.
*
* @return The value of the parsed Exp-Golomb-coded integer.
*/
public int readUnsignedExpGolombCodedInt() {
return readExpGolombCodeNum();
}
/**
* Reads an signed Exp-Golomb-coded format integer.
*
* @return The value of the parsed Exp-Golomb-coded integer.
*/
public int readSignedExpGolombCodedInt() {
int codeNum = readExpGolombCodeNum();
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()) {
leadingZeros++;
}
return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0);
}
}
...@@ -23,18 +23,69 @@ import java.nio.ByteBuffer; ...@@ -23,18 +23,69 @@ import java.nio.ByteBuffer;
*/ */
public final class ParsableByteArray { public final class ParsableByteArray {
public final byte[] data; public byte[] data;
private int position; private int position;
private int limit;
/** Creates a new parsable array with {@code length} bytes. */ /** Creates a new instance that initially has no backing data. */
public ParsableByteArray() {}
/** 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];
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, 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;
}
/** Returns the number of bytes yet to be read. */
public int bytesLeft() {
return limit - position;
} }
/** Returns the number of bytes in the array. */ /** Returns the limit. */
public int length() { public int limit() {
return data.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. */ /** Returns the current offset in the array, in bytes. */
...@@ -42,6 +93,11 @@ public final class ParsableByteArray { ...@@ -42,6 +93,11 @@ public final class ParsableByteArray {
return position; 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. * Sets the reading offset in the array.
* *
...@@ -51,7 +107,7 @@ public final class ParsableByteArray { ...@@ -51,7 +107,7 @@ public final class ParsableByteArray {
*/ */
public void setPosition(int position) { public void setPosition(int position) {
// It is fine for position to be at the end of the array. // 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; this.position = position;
} }
...@@ -61,11 +117,27 @@ public final class ParsableByteArray { ...@@ -61,11 +117,27 @@ public final class ParsableByteArray {
* @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the
* array. * 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) { public void skip(int bytes) {
setPosition(position + 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}. * Reads the next {@code length} bytes into {@code buffer} at {@code offset}.
* *
* @see System#arraycopy * @see System#arraycopy
...@@ -128,6 +200,22 @@ public final class ParsableByteArray { ...@@ -128,6 +200,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