diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 88b5eb5a93..596cd5cacc 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index 6c5bb7536a..6f04db8991 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -92,9 +92,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private DemoPlayer player; private boolean playerNeedsPrepare; - private boolean autoPlay = true; private long playerPosition; - private boolean enableBackgroundAudio = false; + private boolean enableBackgroundAudio; private Uri contentUri; private int contentType; @@ -166,10 +165,10 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, if (!enableBackgroundAudio) { releasePlayer(); } else { - player.blockingClearSurface(); + player.setBackgrounded(true); } - audioCapabilitiesReceiver.unregister(); + shutterView.setVisibility(View.VISIBLE); } @Override @@ -183,7 +182,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, @Override public void onClick(View view) { if (view == retryButton) { - autoPlay = true; preparePlayer(); } } @@ -192,11 +190,14 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, @Override public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { - this.audioCapabilities = audioCapabilities; - releasePlayer(); - - autoPlay = true; - preparePlayer(); + boolean audioCapabilitiesChanged = !audioCapabilities.equals(this.audioCapabilities); + if (player == null || audioCapabilitiesChanged) { + this.audioCapabilities = audioCapabilities; + releasePlayer(); + preparePlayer(); + } else if (player != null) { + player.setBackgrounded(false); + } } // Internal methods @@ -239,15 +240,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, updateButtonVisibilities(); } player.setSurface(surfaceView.getHolder().getSurface()); - maybeStartPlayback(); - } - - private void maybeStartPlayback() { - if (autoPlay && (player.getSurface().isValid() - || player.getSelectedTrackIndex(DemoPlayer.TYPE_VIDEO) == DemoPlayer.DISABLED_TRACK)) { - player.setPlayWhenReady(true); - autoPlay = false; - } + player.setPlayWhenReady(true); } private void releasePlayer() { @@ -468,7 +461,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, public void surfaceCreated(SurfaceHolder holder) { if (player != null) { player.setSurface(holder.getSurface()); - maybeStartPlayback(); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index e20b87c21a..338ec5b1db 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -179,10 +179,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private Surface surface; private InternalRendererBuilderCallback builderCallback; private TrackRenderer videoRenderer; + private int videoTrackToRestore; private MultiTrackChunkSource[] multiTrackSources; private String[][] trackNames; private int[] selectedTracks; + private boolean backgrounded; private TextListener textListener; private Id3MetadataListener id3MetadataListener; @@ -233,7 +235,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public void setSurface(Surface surface) { this.surface = surface; - pushSurfaceAndVideoTrack(false); + pushSurface(false); } public Surface getSurface() { @@ -242,7 +244,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public void blockingClearSurface() { surface = null; - pushSurfaceAndVideoTrack(true); + pushSurface(true); } public String[] getTracks(int type) { @@ -258,13 +260,23 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi return; } selectedTracks[type] = index; - if (type == TYPE_VIDEO) { - pushSurfaceAndVideoTrack(false); + pushTrackSelection(type, true); + if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) { + textListener.onText(null); + } + } + + public void setBackgrounded(boolean backgrounded) { + if (this.backgrounded == backgrounded) { + return; + } + this.backgrounded = backgrounded; + if (backgrounded) { + videoTrackToRestore = getSelectedTrackIndex(TYPE_VIDEO); + selectTrack(TYPE_VIDEO, DISABLED_TRACK); + blockingClearSurface(); } else { - pushTrackSelection(type, true); - if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) { - textListener.onText(null); - } + selectTrack(TYPE_VIDEO, videoTrackToRestore); } } @@ -307,7 +319,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi this.trackNames = trackNames; this.multiTrackSources = multiTrackSources; rendererBuildingState = RENDERER_BUILDING_STATE_BUILT; - pushSurfaceAndVideoTrack(false); + pushSurface(false); + pushTrackSelection(TYPE_VIDEO, true); pushTrackSelection(TYPE_AUDIO, true); pushTrackSelection(TYPE_TEXT, true); player.prepare(renderers); @@ -550,7 +563,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } } - private void pushSurfaceAndVideoTrack(boolean blockForSurfacePush) { + private void pushSurface(boolean blockForSurfacePush) { if (rendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { return; } @@ -562,7 +575,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi player.sendMessage( videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); } - pushTrackSelection(TYPE_VIDEO, surface != null && surface.isValid()); } private void pushTrackSelection(int type, boolean allowRendererEnable) { diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java index f710b0d7a7..6bf5e08c42 100644 --- a/library/src/main/java/com/google/android/exoplayer/C.java +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer; +import android.media.MediaExtractor; + /** * Defines constants that are generally useful throughout the library. */ @@ -40,6 +42,12 @@ public final class C { */ public static final String UTF8_NAME = "UTF-8"; + /** + * Sample flag that indicates the sample is a synchronization sample. + */ + @SuppressWarnings("InlinedApi") + public static final int SAMPLE_FLAG_SYNC = MediaExtractor.SAMPLE_FLAG_SYNC; + private C() {} } diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java index 6fb7d20be8..67b902aff9 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java @@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - public static final String VERSION = "1.1.0"; + public static final String VERSION = "1.2.0"; /** * The version of the library, expressed as an integer. @@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo { * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * corresponding integer version 001002003. */ - public static final int VERSION_INT = 001001000; + public static final int VERSION_INT = 001002000; /** * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions} diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 6cbd6248a1..61b12e33b1 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -440,17 +440,14 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { checkForDiscontinuity(); if (format == null) { readFormat(); - } else if (codec == null && !shouldInitCodec() && getState() == TrackRenderer.STATE_STARTED) { - discardSamples(positionUs); - } else { - if (codec == null && shouldInitCodec()) { - maybeInitCodec(); - } - if (codec != null) { - while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} - if (feedInputBuffer(true)) { - while (feedInputBuffer(false)) {} - } + } + if (codec == null && shouldInitCodec()) { + maybeInitCodec(); + } + if (codec != null) { + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} + if (feedInputBuffer(true)) { + while (feedInputBuffer(false)) {} } } codecCounters.ensureUpdated(); @@ -466,21 +463,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } } - private void discardSamples(long positionUs) throws IOException, ExoPlaybackException { - sampleHolder.data = null; - int result = SampleSource.SAMPLE_READ; - while (result == SampleSource.SAMPLE_READ && currentPositionUs <= positionUs) { - result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false); - if (result == SampleSource.SAMPLE_READ) { - if (!sampleHolder.decodeOnly) { - currentPositionUs = sampleHolder.timeUs; - } - } else if (result == SampleSource.FORMAT_READ) { - onInputFormatChanged(formatHolder); - } - } - } - private void checkForDiscontinuity() throws IOException, ExoPlaybackException { if (codec == null) { return; @@ -590,7 +572,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { if (waitingForFirstSyncFrame) { // TODO: Find out if it's possible to supply samples prior to the first sync // frame for HE-AAC. - if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) == 0) { + if ((sampleHolder.flags & C.SAMPLE_FLAG_SYNC) == 0) { sampleHolder.data.clear(); if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { // The buffer we just cleared contained reconfiguration data. We need to re-write this diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java index 397f83cb88..685b32eff3 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -353,7 +353,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { @Override protected boolean shouldInitCodec() { - return super.shouldInitCodec() && surface != null; + return super.shouldInitCodec() && surface != null && surface.isValid(); } // Override configureCodec to provide the surface. diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index 0814f6b5bb..9c33014da8 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -479,7 +479,9 @@ public final class AudioTrack { /** Returns whether enough data has been supplied via {@link #handleBuffer} to begin playback. */ public boolean hasEnoughDataToBeginPlayback() { - return submittedBytes >= minBufferSize; + // The value of minBufferSize can be slightly less than what's actually required for playback + // to start, hence the multiplication factor. + return submittedBytes > (minBufferSize * 3) / 2; } /** Sets the playback volume. */ diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java index 274a4664aa..d6229cf805 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java @@ -638,7 +638,7 @@ public final class FragmentedMp4Extractor implements Extractor { } Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); - out.initEncryptionData(senc.length() - senc.getPosition()); + out.initEncryptionData(senc.bytesLeft()); out.fillEncryptionData(senc); } @@ -696,7 +696,7 @@ public final class FragmentedMp4Extractor implements Extractor { offset += sizes[i]; } - return new SegmentIndex(atom.length(), sizes, offsets, durationsUs, timesUs); + return new SegmentIndex(atom.limit(), sizes, offsets, durationsUs, timesUs); } private int readEncryptionData(NonBlockingInputStream inputStream) { @@ -762,7 +762,6 @@ public final class FragmentedMp4Extractor implements Extractor { return 0; } - @SuppressLint("InlinedApi") private int readSample(NonBlockingInputStream inputStream, int sampleSize, SampleHolder out) { if (out == null) { return RESULT_NEED_SAMPLE_HOLDER; @@ -770,7 +769,7 @@ public final class FragmentedMp4Extractor implements Extractor { out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L; out.flags = 0; if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) { - out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC; + out.flags |= C.SAMPLE_FLAG_SYNC; lastSyncSampleIndex = sampleIndex; } if (out.data == null || out.data.capacity() < sampleSize) { diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java index 9d4f8cc3de..eb16a87650 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java @@ -113,7 +113,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; * @param length The length in bytes of the encryption data. */ public void initEncryptionData(int length) { - if (sampleEncryptionData == null || sampleEncryptionData.length() < length) { + if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) { sampleEncryptionData = new ParsableByteArray(length); } sampleEncryptionDataLength = length; diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java index 829d604a77..ddf15e5610 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java @@ -25,9 +25,6 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.util.LongArray; import com.google.android.exoplayer.util.MimeTypes; -import android.annotation.TargetApi; -import android.media.MediaExtractor; - import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -42,7 +39,6 @@ import java.util.concurrent.TimeUnit; * Matroska is available here. * More info about WebM is here. */ -@TargetApi(16) public final class WebmExtractor implements Extractor { private static final String DOC_TYPE_WEBM = "webm"; @@ -412,7 +408,7 @@ public final class WebmExtractor implements Extractor { case LACING_NONE: long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes; simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs; - sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; + sampleHolder.flags = keyframe ? C.SAMPLE_FLAG_SYNC : 0; sampleHolder.decodeOnly = invisible; sampleHolder.timeUs = clusterTimecodeUs + timecodeUs; sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead()); diff --git a/library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/DataChunk.java similarity index 68% rename from library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java rename to library/src/main/java/com/google/android/exoplayer/hls/DataChunk.java index cc56691441..04d1bc2728 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/DataChunk.java @@ -17,19 +17,21 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; -import com.google.android.exoplayer.util.BitArray; import java.io.IOException; +import java.util.Arrays; /** * An abstract base class for {@link HlsChunk} implementations where the data should be loaded into - * a {@link BitArray} and subsequently consumed. + * a {@code byte[]} before being consumed. */ -public abstract class BitArrayChunk extends HlsChunk { +public abstract class DataChunk extends HlsChunk { private static final int READ_GRANULARITY = 16 * 1024; - private final BitArray bitArray; + private byte[] data; + private int limit; + private volatile boolean loadFinished; private volatile boolean loadCanceled; @@ -39,26 +41,27 @@ public abstract class BitArrayChunk extends HlsChunk { * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then * the length resolved by {@code dataSource.open(dataSpec)} must not exceed * {@link Integer#MAX_VALUE}. - * @param bitArray The {@link BitArray} into which the data should be loaded. + * @param data An optional recycled array that can be used as a holder for the data. */ - public BitArrayChunk(DataSource dataSource, DataSpec dataSpec, BitArray bitArray) { + public DataChunk(DataSource dataSource, DataSpec dataSpec, byte[] data) { super(dataSource, dataSpec); - this.bitArray = bitArray; + this.data = data; } @Override public void consume() throws IOException { - consume(bitArray); + consume(data, limit); } /** * Invoked by {@link #consume()}. Implementations should override this method to consume the * loaded data. * - * @param bitArray The {@link BitArray} containing the loaded data. + * @param data An array containing the data. + * @param limit The limit of the data. * @throws IOException If an error occurs consuming the loaded data. */ - protected abstract void consume(BitArray bitArray) throws IOException; + protected abstract void consume(byte[] data, int limit) throws IOException; /** * Whether the whole of the chunk has been loaded. @@ -85,11 +88,15 @@ public abstract class BitArrayChunk extends HlsChunk { @Override public final void load() throws IOException, InterruptedException { try { - bitArray.reset(); dataSource.open(dataSpec); + limit = 0; int bytesRead = 0; while (bytesRead != -1 && !loadCanceled) { - bytesRead = bitArray.append(dataSource, READ_GRANULARITY); + maybeExpandData(); + bytesRead = dataSource.read(data, limit, READ_GRANULARITY); + if (bytesRead != -1) { + limit += bytesRead; + } } loadFinished = !loadCanceled; } finally { @@ -97,4 +104,14 @@ public abstract class BitArrayChunk extends HlsChunk { } } + private void maybeExpandData() { + if (data == null) { + data = new byte[READ_GRANULARITY]; + } else if (data.length < limit + READ_GRANULARITY) { + // The new length is calculated as (data.length + READ_GRANULARITY) rather than + // (limit + READ_GRANULARITY) in order to avoid small increments in the length. + data = Arrays.copyOf(data, data.length + READ_GRANULARITY); + } + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index 584372c86a..90bc497478 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -17,14 +17,14 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.hls.TsExtractor.SamplePool; +import com.google.android.exoplayer.hls.parser.TsExtractor; import com.google.android.exoplayer.upstream.Aes128DataSource; import com.google.android.exoplayer.upstream.BandwidthMeter; +import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -35,6 +35,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigInteger; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -81,6 +82,11 @@ public class HlsChunkSource { */ public static final int ADAPTIVE_MODE_ABRUPT = 3; + /** + * The default target buffer size in bytes. + */ + public static final int DEFAULT_TARGET_BUFFER_SIZE = 18 * 1024 * 1024; + /** * The default target buffer duration in milliseconds. */ @@ -101,20 +107,21 @@ public class HlsChunkSource { private static final String TAG = "HlsChunkSource"; private static final float BANDWIDTH_FRACTION = 0.8f; - private final SamplePool samplePool = new TsExtractor.SamplePool(); + private final BufferPool bufferPool; private final DataSource upstreamDataSource; private final HlsPlaylistParser playlistParser; private final Variant[] enabledVariants; private final BandwidthMeter bandwidthMeter; - private final BitArray bitArray; private final int adaptiveMode; private final Uri baseUri; private final int maxWidth; private final int maxHeight; + private final int targetBufferSize; private final long targetBufferDurationUs; private final long minBufferDurationToSwitchUpUs; private final long maxBufferDurationToSwitchDownUs; + /* package */ byte[] scratchSpace; /* package */ final HlsMediaPlaylist[] mediaPlaylists; /* package */ final boolean[] mediaPlaylistBlacklistFlags; /* package */ final long[] lastMediaPlaylistLoadTimesMs; @@ -130,8 +137,8 @@ public class HlsChunkSource { public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) { this(dataSource, playlistUrl, playlist, bandwidthMeter, variantIndices, adaptiveMode, - DEFAULT_TARGET_BUFFER_DURATION_MS, DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, - DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS); + DEFAULT_TARGET_BUFFER_SIZE, DEFAULT_TARGET_BUFFER_DURATION_MS, + DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS); } /** @@ -144,9 +151,10 @@ public class HlsChunkSource { * @param adaptiveMode The mode for switching from one variant to another. One of * {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and * {@link #ADAPTIVE_MODE_SPLICE}. + * @param targetBufferSize The targeted buffer size in bytes. The buffer will not be filled more + * than one chunk beyond this amount of data. * @param targetBufferDurationMs The targeted duration of media to buffer ahead of the current - * playback position. Note that the greater this value, the greater the amount of memory - * that will be consumed. + * playback position. The buffer will not be filled more than one chunk beyond this position. * @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered * for a switch to a higher quality variant to be considered. * @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered @@ -154,17 +162,18 @@ public class HlsChunkSource { */ public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode, - long targetBufferDurationMs, long minBufferDurationToSwitchUpMs, + int targetBufferSize, long targetBufferDurationMs, long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs) { this.upstreamDataSource = dataSource; this.bandwidthMeter = bandwidthMeter; this.adaptiveMode = adaptiveMode; + this.targetBufferSize = targetBufferSize; targetBufferDurationUs = targetBufferDurationMs * 1000; minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000; maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000; baseUri = playlist.baseUri; - bitArray = new BitArray(); playlistParser = new HlsPlaylistParser(); + bufferPool = new BufferPool(256 * 1024); if (playlist.type == HlsPlaylist.TYPE_MEDIA) { enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)}; @@ -225,8 +234,9 @@ public class HlsChunkSource { public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs, long playbackPositionUs) { if (previousTsChunk != null && (previousTsChunk.isLastChunk - || previousTsChunk.endTimeUs - playbackPositionUs >= targetBufferDurationUs)) { - // We're either finished, or we have the target amount of data buffered. + || previousTsChunk.endTimeUs - playbackPositionUs >= targetBufferDurationUs) + || bufferPool.getAllocatedSize() >= targetBufferSize) { + // We're either finished, or we have the target amount of data or time buffered. return null; } @@ -324,7 +334,7 @@ public class HlsChunkSource { // Configure the extractor that will read the chunk. TsExtractor extractor; if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) { - extractor = new TsExtractor(startTimeUs, samplePool, switchingVariantSpliced); + extractor = new TsExtractor(startTimeUs, switchingVariantSpliced, bufferPool); } else { extractor = previousTsChunk.extractor; } @@ -526,7 +536,7 @@ public class HlsChunkSource { return true; } - private class MediaPlaylistChunk extends BitArrayChunk { + private class MediaPlaylistChunk extends DataChunk { @SuppressWarnings("hiding") /* package */ final int variantIndex; @@ -535,37 +545,38 @@ public class HlsChunkSource { public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec, Uri playlistBaseUri) { - super(dataSource, dataSpec, bitArray); + super(dataSource, dataSpec, scratchSpace); this.variantIndex = variantIndex; this.playlistBaseUri = playlistBaseUri; } @Override - protected void consume(BitArray data) throws IOException { - HlsPlaylist playlist = playlistParser.parse( - new ByteArrayInputStream(data.getData(), 0, data.bytesLeft()), null, null, - playlistBaseUri); + protected void consume(byte[] data, int limit) throws IOException { + HlsPlaylist playlist = playlistParser.parse(new ByteArrayInputStream(data, 0, limit), + null, null, playlistBaseUri); Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MEDIA); HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; setMediaPlaylist(variantIndex, mediaPlaylist); + // Recycle the allocation. + scratchSpace = data; } } - private class EncryptionKeyChunk extends BitArrayChunk { + private class EncryptionKeyChunk extends DataChunk { private final String iv; public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) { - super(dataSource, dataSpec, bitArray); + super(dataSource, dataSpec, scratchSpace); this.iv = iv; } @Override - protected void consume(BitArray data) throws IOException { - byte[] secretKey = new byte[data.bytesLeft()]; - data.readBytes(secretKey, 0, secretKey.length); - initEncryptedDataSource(dataSpec.uri, iv, secretKey); + protected void consume(byte[] data, int limit) throws IOException { + initEncryptedDataSource(dataSpec.uri, iv, Arrays.copyOf(data, limit)); + // Recycle the allocation. + scratchSpace = data; } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index ea6fddd488..4603577ff9 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.hls.parser.TsExtractor; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.util.Assertions; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java index 04a3f9200f..36c1e30c8f 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.hls; +import com.google.android.exoplayer.hls.parser.TsExtractor; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java deleted file mode 100644 index 5452391e24..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ /dev/null @@ -1,1270 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer.hls; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.text.eia608.Eia608Parser; -import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.BitArray; -import com.google.android.exoplayer.util.CodecSpecificDataUtil; -import com.google.android.exoplayer.util.MimeTypes; - -import android.annotation.SuppressLint; -import android.media.MediaExtractor; -import android.util.Log; -import android.util.Pair; -import android.util.SparseArray; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.TreeSet; -import java.util.concurrent.ConcurrentLinkedQueue; - -/** - * Facilitates the extraction of data from the MPEG-2 TS container format. - */ -public final class TsExtractor { - - private static final String TAG = "TsExtractor"; - - private static final int TS_PACKET_SIZE = 188; - private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. - private static final int TS_PAT_PID = 0; - - private static final int TS_STREAM_TYPE_AAC = 0x0F; - private static final int TS_STREAM_TYPE_H264 = 0x1B; - private static final int TS_STREAM_TYPE_ID3 = 0x15; - private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 - - private static final long MAX_PTS = 0x1FFFFFFFFL; - - private final BitArray tsPacketBuffer; - private final SparseArray sampleQueues; // Indexed by streamType - private final SparseArray tsPayloadReaders; // Indexed by pid - private final SamplePool samplePool; - private final boolean shouldSpliceIn; - private final long firstSampleTimestamp; - - // Accessed only by the consuming thread. - private boolean spliceConfigured; - - // Accessed only by the loading thread. - private long timestampOffsetUs; - private long lastPts; - - // Accessed by both the loading and consuming threads. - private volatile boolean prepared; - /* package */ volatile long largestParsedTimestampUs; - - public TsExtractor(long firstSampleTimestamp, SamplePool samplePool, boolean shouldSpliceIn) { - this.firstSampleTimestamp = firstSampleTimestamp; - this.samplePool = samplePool; - this.shouldSpliceIn = shouldSpliceIn; - tsPacketBuffer = new BitArray(); - sampleQueues = new SparseArray(); - tsPayloadReaders = new SparseArray(); - tsPayloadReaders.put(TS_PAT_PID, new PatReader()); - largestParsedTimestampUs = Long.MIN_VALUE; - lastPts = Long.MIN_VALUE; - } - - /** - * Gets the number of available tracks. - *

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

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

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

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

- * Splice configuration may fail if the next extractor is not yet in a state that allows the - * splice to be performed. Calling this method is a noop if the splice has already been - * configured. Hence this method should be called repeatedly during the window within which a - * splice can be performed. - * - * @param nextExtractor The extractor being spliced to. - */ - public void configureSpliceTo(TsExtractor nextExtractor) { - Assertions.checkState(prepared); - if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) { - // The splice is already configured, or the next extractor doesn't want to be spliced in, or - // the next extractor isn't ready to be spliced in. - return; - } - boolean spliceConfigured = true; - for (int i = 0; i < sampleQueues.size(); i++) { - spliceConfigured &= sampleQueues.valueAt(i).configureSpliceTo( - nextExtractor.sampleQueues.valueAt(i)); - } - this.spliceConfigured = spliceConfigured; - return; - } - - /** - * Gets the largest timestamp of any sample parsed by the extractor. - * - * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed. - */ - public long getLargestSampleTimestamp() { - return largestParsedTimestampUs; - } - - /** - * Gets the next sample for the specified track. - * - * @param track The track from which to read. - * @param out A {@link SampleHolder} into which the next sample should be read. - * @return True if a sample was read. False otherwise. - */ - public boolean getSample(int track, SampleHolder out) { - Assertions.checkState(prepared); - SampleQueue sampleQueue = sampleQueues.valueAt(track); - Sample sample = sampleQueue.poll(); - if (sample == null) { - return false; - } - convert(sample, out); - sampleQueue.recycle(sample); - return true; - } - - /** - * Discards samples for the specified track up to the specified time. - * - * @param track The track from which samples should be discarded. - * @param timeUs The time up to which samples should be discarded, in microseconds. - */ - public void discardUntil(int track, long timeUs) { - Assertions.checkState(prepared); - sampleQueues.valueAt(track).discardUntil(timeUs); - } - - /** - * Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the - * specified track. - * - * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)} - * for the specified track. False otherwise. - */ - public boolean hasSamples(int track) { - Assertions.checkState(prepared); - return sampleQueues.valueAt(track).peek() != null; - } - - private boolean checkPrepared() { - int pesPayloadReaderCount = sampleQueues.size(); - if (pesPayloadReaderCount == 0) { - return false; - } - for (int i = 0; i < pesPayloadReaderCount; i++) { - if (!sampleQueues.valueAt(i).hasMediaFormat()) { - return false; - } - } - return true; - } - - /** - * Reads up to a single TS packet. - * - * @param dataSource The {@link DataSource} from which to read. - * @throws IOException If an error occurred reading from the source. - * @return The number of bytes read from the source. - */ - public int read(DataSource dataSource) throws IOException { - int read = tsPacketBuffer.append(dataSource, TS_PACKET_SIZE - tsPacketBuffer.bytesLeft()); - if (read == -1) { - return -1; - } - - if (tsPacketBuffer.bytesLeft() != TS_PACKET_SIZE) { - return read; - } - - // Parse TS header. - // Check sync byte. - int syncByte = tsPacketBuffer.readUnsignedByte(); - if (syncByte != TS_SYNC_BYTE) { - return read; - } - - // Skip transportErrorIndicator. - tsPacketBuffer.skipBits(1); - boolean payloadUnitStartIndicator = tsPacketBuffer.readBit(); - // Skip transportPriority. - tsPacketBuffer.skipBits(1); - int pid = tsPacketBuffer.readBits(13); - // Skip transport_scrambling_control. - tsPacketBuffer.skipBits(2); - boolean adaptationFieldExists = tsPacketBuffer.readBit(); - boolean payloadExists = tsPacketBuffer.readBit(); - // Skip continuityCounter. - tsPacketBuffer.skipBits(4); - - // Read the adaptation field. - if (adaptationFieldExists) { - int adaptationFieldLength = tsPacketBuffer.readBits(8); - tsPacketBuffer.skipBytes(adaptationFieldLength); - } - - // Read Payload. - if (payloadExists) { - TsPayloadReader payloadReader = tsPayloadReaders.get(pid); - if (payloadReader != null) { - payloadReader.read(tsPacketBuffer, payloadUnitStartIndicator); - } - } - - if (!prepared) { - prepared = checkPrepared(); - } - - tsPacketBuffer.reset(); - return read; - } - - @SuppressLint("InlinedApi") - private void convert(Sample in, SampleHolder out) { - if (out.data == null || out.data.capacity() < in.size) { - out.replaceBuffer(in.size); - } - if (out.data != null) { - out.data.put(in.data, 0, in.size); - } - out.size = in.size; - out.flags = in.isKeyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; - out.timeUs = in.timeUs; - } - - /** - * Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound. - * - * @param pts The raw PTS value. - * @return The corresponding time in microseconds. - */ - /* package */ long ptsToTimeUs(long pts) { - if (lastPts != Long.MIN_VALUE) { - // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), - // and we need to snap to the one closest to lastPts. - long closestWrapCount = (lastPts + (MAX_PTS / 2)) / MAX_PTS; - long ptsWrapBelow = pts + (MAX_PTS * (closestWrapCount - 1)); - long ptsWrapAbove = pts + (MAX_PTS * closestWrapCount); - pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts) - ? ptsWrapBelow : ptsWrapAbove; - } - // Calculate the corresponding timestamp. - long timeUs = (pts * C.MICROS_PER_SECOND) / 90000; - // If we haven't done the initial timestamp adjustment, do it now. - if (lastPts == Long.MIN_VALUE) { - timestampOffsetUs = firstSampleTimestamp - timeUs; - } - // Record the adjusted PTS to adjust for wraparound next time. - lastPts = pts; - return timeUs + timestampOffsetUs; - } - - /** - * Parses payload data. - */ - private abstract static class TsPayloadReader { - - public abstract void read(BitArray tsBuffer, boolean payloadUnitStartIndicator); - - } - - /** - * Parses Program Association Table data. - */ - private class PatReader extends TsPayloadReader { - - @Override - public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { - // Skip pointer. - if (payloadUnitStartIndicator) { - int pointerField = tsBuffer.readBits(8); - tsBuffer.skipBytes(pointerField); - } - - tsBuffer.skipBits(12); // 8+1+1+2 - int sectionLength = tsBuffer.readBits(12); - tsBuffer.skipBits(40); // 16+2+5+1+8+8 - - int programCount = (sectionLength - 9) / 4; - for (int i = 0; i < programCount; i++) { - tsBuffer.skipBits(19); - int pid = tsBuffer.readBits(13); - tsPayloadReaders.put(pid, new PmtReader()); - } - - // Skip CRC_32. - } - - } - - /** - * Parses Program Map Table. - */ - private class PmtReader extends TsPayloadReader { - - @Override - public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { - // Skip pointer. - if (payloadUnitStartIndicator) { - int pointerField = tsBuffer.readBits(8); - tsBuffer.skipBytes(pointerField); - } - - // Skip table_id, section_syntax_indicator, etc. - tsBuffer.skipBits(12); // 8+1+1+2 - int sectionLength = tsBuffer.readBits(12); - // Skip the rest of the PMT header. - tsBuffer.skipBits(60); // 16+2+5+1+8+8+3+13+4 - - int programInfoLength = tsBuffer.readBits(12); - // Skip the descriptors. - tsBuffer.skipBytes(programInfoLength); - - int entriesSize = sectionLength - 9 /* size of the rest of the fields before descriptors */ - - programInfoLength - 4 /* CRC size */; - while (entriesSize > 0) { - int streamType = tsBuffer.readBits(8); - tsBuffer.skipBits(3); - int elementaryPid = tsBuffer.readBits(13); - tsBuffer.skipBits(4); - - int esInfoLength = tsBuffer.readBits(12); - // Skip the descriptors. - tsBuffer.skipBytes(esInfoLength); - entriesSize -= esInfoLength + 5; - - if (sampleQueues.get(streamType) != null) { - continue; - } - - PesPayloadReader pesPayloadReader = null; - switch (streamType) { - case TS_STREAM_TYPE_AAC: - pesPayloadReader = new AdtsReader(samplePool); - break; - case TS_STREAM_TYPE_H264: - SeiReader seiReader = new SeiReader(samplePool); - sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader); - pesPayloadReader = new H264Reader(samplePool, seiReader); - break; - case TS_STREAM_TYPE_ID3: - pesPayloadReader = new Id3Reader(samplePool); - break; - } - - if (pesPayloadReader != null) { - sampleQueues.put(streamType, pesPayloadReader); - tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader)); - } - } - - // Skip CRC_32. - } - - } - - /** - * Parses PES packet data and extracts samples. - */ - private class PesReader extends TsPayloadReader { - - // Reusable buffer for incomplete PES data. - private final BitArray pesBuffer; - // Parses PES payload and extracts individual samples. - private final PesPayloadReader pesPayloadReader; - - private int packetLength; - - public PesReader(PesPayloadReader pesPayloadReader) { - this.pesPayloadReader = pesPayloadReader; - this.packetLength = -1; - pesBuffer = new BitArray(); - } - - @Override - public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { - if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) { - if (packetLength == 0) { - // The length of the previous packet was unspecified. We've now seen the start of the - // next one, so consume the previous packet's body. - readPacketBody(); - } else { - // Either we didn't have enough data to read the length of the previous packet, or we - // did read the length but didn't receive that amount of data. Neither case is expected. - Log.w(TAG, "Unexpected packet fragment of length " + pesBuffer.bytesLeft()); - pesBuffer.reset(); - packetLength = -1; - } - } - - pesBuffer.append(tsBuffer, tsBuffer.bytesLeft()); - - if (packetLength == -1 && pesBuffer.bytesLeft() >= 6) { - // We haven't read the start of the packet, but have enough data to do so. - readPacketStart(); - } - if (packetLength > 0 && pesBuffer.bytesLeft() >= packetLength) { - // The packet length was specified and we now have the whole packet. Read it. - readPacketBody(); - } - } - - private void readPacketStart() { - int startCodePrefix = pesBuffer.readBits(24); - if (startCodePrefix != 0x000001) { - Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); - pesBuffer.reset(); - packetLength = -1; - } else { - // TODO: Read and use stream_id. - pesBuffer.skipBits(8); // Skip stream_id. - packetLength = pesBuffer.readBits(16); - } - } - - private void readPacketBody() { - // Skip some fields/flags. - // TODO: might need to use data_alignment_indicator. - pesBuffer.skipBits(8); // 2+2+1+1+1+1 - boolean ptsFlag = pesBuffer.readBit(); - // Skip DTS flag. - pesBuffer.skipBits(1); - // Skip some fields/flags. - pesBuffer.skipBits(6); // 1+1+1+1+1+1 - - int headerDataLength = pesBuffer.readBits(8); - if (headerDataLength == 0) { - headerDataLength = pesBuffer.bytesLeft(); - } - - long timeUs = 0; - if (ptsFlag) { - // Skip prefix. - pesBuffer.skipBits(4); - long pts = pesBuffer.readBitsLong(3) << 30; - pesBuffer.skipBits(1); - pts |= pesBuffer.readBitsLong(15) << 15; - pesBuffer.skipBits(1); - pts |= pesBuffer.readBitsLong(15); - pesBuffer.skipBits(1); - timeUs = ptsToTimeUs(pts); - // Skip the rest of the header. - pesBuffer.skipBytes(headerDataLength - 5); - } else { - // Skip the rest of the header. - pesBuffer.skipBytes(headerDataLength); - } - - int payloadSize; - if (packetLength == 0) { - // If pesPacketLength is not specified read all available data. - payloadSize = pesBuffer.bytesLeft(); - } else { - payloadSize = packetLength - headerDataLength - 3; - } - - pesPayloadReader.read(pesBuffer, payloadSize, timeUs); - pesBuffer.reset(); - packetLength = -1; - } - - } - - /** - * A queue of extracted samples together with their corresponding {@link MediaFormat}. - */ - private abstract class SampleQueue { - - @SuppressWarnings("hiding") - private final SamplePool samplePool; - - // Accessed only by the consuming thread. - private boolean needKeyframe; - private long lastReadTimeUs; - private long spliceOutTimeUs; - - // Accessed by both the loading and consuming threads. - private volatile MediaFormat mediaFormat; - - protected SampleQueue(SamplePool samplePool) { - this.samplePool = samplePool; - needKeyframe = true; - lastReadTimeUs = Long.MIN_VALUE; - spliceOutTimeUs = Long.MIN_VALUE; - } - - public boolean hasMediaFormat() { - return mediaFormat != null; - } - - public MediaFormat getMediaFormat() { - return mediaFormat; - } - - protected void setMediaFormat(MediaFormat mediaFormat) { - this.mediaFormat = mediaFormat; - } - - /** - * Removes and returns the next sample from the queue. - *

- * The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples - * queued prior to the first keyframe are discarded. - * - * @return The next sample from the queue, or null if a sample isn't available. - */ - public Sample poll() { - Sample head = peek(); - if (head != null) { - internalPollSample(); - needKeyframe = false; - lastReadTimeUs = head.timeUs; - } - return head; - } - - /** - * Like {@link #poll()}, except the returned sample is not removed from the queue. - * - * @return The next sample from the queue, or null if a sample isn't available. - */ - public Sample peek() { - Sample head = internalPeekSample(); - if (needKeyframe) { - // Peeking discard of samples until we find a keyframe or run out of available samples. - while (head != null && !head.isKeyframe) { - recycle(head); - internalPollSample(); - head = internalPeekSample(); - } - } - if (head == null) { - return null; - } - if (spliceOutTimeUs != Long.MIN_VALUE && head.timeUs >= spliceOutTimeUs) { - // The sample is later than the time this queue is spliced out. - recycle(head); - internalPollSample(); - return null; - } - return head; - } - - /** - * Discards samples from the queue up to the specified time. - * - * @param timeUs The time up to which samples should be discarded, in microseconds. - */ - public void discardUntil(long timeUs) { - Sample head = peek(); - while (head != null && head.timeUs < timeUs) { - recycle(head); - internalPollSample(); - head = internalPeekSample(); - // We're discarding at least one sample, so any subsequent read will need to start at - // a keyframe. - needKeyframe = true; - } - lastReadTimeUs = Long.MIN_VALUE; - } - - /** - * Clears the queue. - */ - public void release() { - Sample toRecycle = internalPollSample(); - while (toRecycle != null) { - recycle(toRecycle); - toRecycle = internalPollSample(); - } - } - - /** - * Recycles a sample. - * - * @param sample The sample to recycle. - */ - public void recycle(Sample sample) { - samplePool.recycle(sample); - } - - /** - * Attempts to configure a splice from this queue to the next. - * - * @param nextQueue The queue being spliced to. - * @return Whether the splice was configured successfully. - */ - public boolean configureSpliceTo(SampleQueue nextQueue) { - if (spliceOutTimeUs != Long.MIN_VALUE) { - // We've already configured the splice. - return true; - } - long firstPossibleSpliceTime; - Sample nextSample = internalPeekSample(); - if (nextSample != null) { - firstPossibleSpliceTime = nextSample.timeUs; - } else { - firstPossibleSpliceTime = lastReadTimeUs + 1; - } - Sample nextQueueSample = nextQueue.internalPeekSample(); - while (nextQueueSample != null - && (nextQueueSample.timeUs < firstPossibleSpliceTime || !nextQueueSample.isKeyframe)) { - // Discard samples from the next queue for as long as they are before the earliest possible - // splice time, or not keyframes. - nextQueue.internalPollSample(); - nextQueueSample = nextQueue.internalPeekSample(); - } - if (nextQueueSample != null) { - // We've found a keyframe in the next queue that can serve as the splice point. Set the - // splice point now. - spliceOutTimeUs = nextQueueSample.timeUs; - return true; - } - return false; - } - - /** - * Obtains a Sample object to use. - * - * @param type The type of the sample. - * @return The sample. - */ - protected Sample getSample(int type) { - return samplePool.get(type); - } - - /** - * Creates a new Sample and adds it to the queue. - * - * @param type The type of the sample. - * @param buffer The buffer to read sample data. - * @param sampleSize The size of the sample data. - * @param sampleTimeUs The sample time stamp. - * @param isKeyframe True if the sample is a keyframe. False otherwise. - */ - protected void addSample(int type, BitArray buffer, int sampleSize, long sampleTimeUs, - boolean isKeyframe) { - Sample sample = getSample(type); - addToSample(sample, buffer, sampleSize); - sample.isKeyframe = isKeyframe; - sample.timeUs = sampleTimeUs; - addSample(sample); - } - - protected void addSample(Sample sample) { - largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); - internalQueueSample(sample); - } - - protected void addToSample(Sample sample, BitArray buffer, int size) { - if (sample.data.length - sample.size < size) { - sample.expand(size - sample.data.length + sample.size); - } - buffer.readBytes(sample.data, sample.size, size); - sample.size += size; - } - - protected abstract Sample internalPeekSample(); - protected abstract Sample internalPollSample(); - protected abstract void internalQueueSample(Sample sample); - - } - - /** - * Extracts individual samples from continuous byte stream, preserving original order. - */ - private abstract class PesPayloadReader extends SampleQueue { - - private final ConcurrentLinkedQueue internalQueue; - - protected PesPayloadReader(SamplePool samplePool) { - super(samplePool); - internalQueue = new ConcurrentLinkedQueue(); - } - - @Override - protected final Sample internalPeekSample() { - return internalQueue.peek(); - } - - @Override - protected final Sample internalPollSample() { - return internalQueue.poll(); - } - - @Override - protected final void internalQueueSample(Sample sample) { - internalQueue.add(sample); - } - - public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs); - - } - - /** - * Parses a continuous H264 byte stream and extracts individual frames. - */ - private class H264Reader extends PesPayloadReader { - - private static final int NAL_UNIT_TYPE_IDR = 5; - private static final int NAL_UNIT_TYPE_SPS = 7; - private static final int NAL_UNIT_TYPE_PPS = 8; - private static final int NAL_UNIT_TYPE_AUD = 9; - - public final SeiReader seiReader; - - // Used to store uncompleted sample data. - private Sample currentSample; - - public H264Reader(SamplePool samplePool, SeiReader seiReader) { - super(samplePool); - this.seiReader = seiReader; - } - - @Override - public void release() { - super.release(); - if (currentSample != null) { - recycle(currentSample); - currentSample = null; - } - } - - @Override - public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { - // Read leftover frame data from previous PES packet. - pesPayloadSize -= readOneH264Frame(pesBuffer, true); - - if (pesBuffer.bytesLeft() <= 0 || pesPayloadSize <= 0) { - return; - } - - // Single PES packet should contain only one new H.264 frame. - if (currentSample != null) { - if (!hasMediaFormat() && currentSample.isKeyframe) { - parseMediaFormat(currentSample); - } - seiReader.read(currentSample.data, currentSample.size, currentSample.timeUs); - addSample(currentSample); - } - currentSample = getSample(Sample.TYPE_VIDEO); - pesPayloadSize -= readOneH264Frame(pesBuffer, false); - currentSample.timeUs = pesTimeUs; - - if (pesPayloadSize > 0) { - Log.e(TAG, "PES packet contains more frame data than expected"); - } - } - - @SuppressLint("InlinedApi") - private int readOneH264Frame(BitArray pesBuffer, boolean remainderOnly) { - int offset = remainderOnly ? 0 : 3; - int audStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_AUD, offset); - if (currentSample != null) { - int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset); - if (idrStart < audStart) { - currentSample.isKeyframe = true; - } - addToSample(currentSample, pesBuffer, audStart); - } else { - pesBuffer.skipBytes(audStart); - } - return audStart; - } - - private void parseMediaFormat(Sample sample) { - BitArray bitArray = new BitArray(sample.data, sample.size); - // Locate the SPS and PPS units. - int spsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_SPS, 0); - int ppsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_PPS, 0); - if (spsOffset == bitArray.bytesLeft() || ppsOffset == bitArray.bytesLeft()) { - return; - } - int spsLength = bitArray.findNextNalUnit(-1, spsOffset + 3) - spsOffset; - int ppsLength = bitArray.findNextNalUnit(-1, ppsOffset + 3) - ppsOffset; - byte[] spsData = new byte[spsLength]; - byte[] ppsData = new byte[ppsLength]; - System.arraycopy(bitArray.getData(), spsOffset, spsData, 0, spsLength); - System.arraycopy(bitArray.getData(), ppsOffset, ppsData, 0, ppsLength); - List initializationData = new ArrayList(); - initializationData.add(spsData); - initializationData.add(ppsData); - - // Unescape the SPS unit. - byte[] unescapedSps = unescapeStream(spsData, 0, spsLength); - bitArray.reset(unescapedSps, unescapedSps.length); - - // Parse the SPS unit - // Skip the NAL header. - bitArray.skipBytes(4); - int profileIdc = bitArray.readBits(8); - // Skip 6 constraint bits, 2 reserved bits and level_idc. - bitArray.skipBytes(2); - // Skip seq_parameter_set_id. - bitArray.readUnsignedExpGolombCodedInt(); - - int chromaFormatIdc = 1; // Default is 4:2:0 - if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244 - || profileIdc == 44 || profileIdc == 83 || profileIdc == 86 || profileIdc == 118 - || profileIdc == 128 || profileIdc == 138) { - chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); - if (chromaFormatIdc == 3) { - // Skip separate_colour_plane_flag - bitArray.skipBits(1); - } - // Skip bit_depth_luma_minus8 - bitArray.readUnsignedExpGolombCodedInt(); - // Skip bit_depth_chroma_minus8 - bitArray.readUnsignedExpGolombCodedInt(); - // Skip qpprime_y_zero_transform_bypass_flag - bitArray.skipBits(1); - boolean seqScalingMatrixPresentFlag = bitArray.readBit(); - if (seqScalingMatrixPresentFlag) { - int limit = (chromaFormatIdc != 3) ? 8 : 12; - for (int i = 0; i < limit; i++) { - boolean seqScalingListPresentFlag = bitArray.readBit(); - if (seqScalingListPresentFlag) { - skipScalingList(bitArray, i < 6 ? 16 : 64); - } - } - } - } - // Skip log2_max_frame_num_minus4 - bitArray.readUnsignedExpGolombCodedInt(); - long picOrderCntType = bitArray.readUnsignedExpGolombCodedInt(); - if (picOrderCntType == 0) { - // Skip log2_max_pic_order_cnt_lsb_minus4 - bitArray.readUnsignedExpGolombCodedInt(); - } else if (picOrderCntType == 1) { - // Skip delta_pic_order_always_zero_flag - bitArray.skipBits(1); - // Skip offset_for_non_ref_pic - bitArray.readSignedExpGolombCodedInt(); - // Skip offset_for_top_to_bottom_field - bitArray.readSignedExpGolombCodedInt(); - long numRefFramesInPicOrderCntCycle = bitArray.readUnsignedExpGolombCodedInt(); - for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) { - // Skip offset_for_ref_frame[i] - bitArray.readUnsignedExpGolombCodedInt(); - } - } - // Skip max_num_ref_frames - bitArray.readUnsignedExpGolombCodedInt(); - // Skip gaps_in_frame_num_value_allowed_flag - bitArray.skipBits(1); - int picWidthInMbs = bitArray.readUnsignedExpGolombCodedInt() + 1; - int picHeightInMapUnits = bitArray.readUnsignedExpGolombCodedInt() + 1; - boolean frameMbsOnlyFlag = bitArray.readBit(); - int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits; - if (!frameMbsOnlyFlag) { - // Skip mb_adaptive_frame_field_flag - bitArray.skipBits(1); - } - // Skip direct_8x8_inference_flag - bitArray.skipBits(1); - int frameWidth = picWidthInMbs * 16; - int frameHeight = frameHeightInMbs * 16; - boolean frameCroppingFlag = bitArray.readBit(); - if (frameCroppingFlag) { - int frameCropLeftOffset = bitArray.readUnsignedExpGolombCodedInt(); - int frameCropRightOffset = bitArray.readUnsignedExpGolombCodedInt(); - int frameCropTopOffset = bitArray.readUnsignedExpGolombCodedInt(); - int frameCropBottomOffset = bitArray.readUnsignedExpGolombCodedInt(); - int cropUnitX, cropUnitY; - if (chromaFormatIdc == 0) { - cropUnitX = 1; - cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0); - } else { - int subWidthC = (chromaFormatIdc == 3) ? 1 : 2; - int subHeightC = (chromaFormatIdc == 1) ? 2 : 1; - cropUnitX = subWidthC; - cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0)); - } - frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX; - frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY; - } - - // Set the format. - setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, - frameWidth, frameHeight, initializationData)); - } - - private void skipScalingList(BitArray bitArray, int size) { - int lastScale = 8; - int nextScale = 8; - for (int i = 0; i < size; i++) { - if (nextScale != 0) { - int deltaScale = bitArray.readSignedExpGolombCodedInt(); - nextScale = (lastScale + deltaScale + 256) % 256; - } - lastScale = (nextScale == 0) ? lastScale : nextScale; - } - } - - /** - * Replaces occurrences of [0, 0, 3] with [0, 0]. - *

- * See ISO/IEC 14496-10:2005(E) page 36 for more information. - */ - private byte[] unescapeStream(byte[] data, int offset, int limit) { - int position = offset; - List escapePositions = new ArrayList(); - while (position < limit) { - position = findNextUnescapeIndex(data, position, limit); - if (position < limit) { - escapePositions.add(position); - position += 3; - } - } - - int escapeCount = escapePositions.size(); - int escapedPosition = offset; // The position being read from. - int unescapedPosition = 0; // The position being written to. - byte[] unescapedData = new byte[limit - offset - escapeCount]; - for (int i = 0; i < escapeCount; i++) { - int nextEscapePosition = escapePositions.get(i); - int copyLength = nextEscapePosition - escapedPosition; - System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, copyLength); - escapedPosition += copyLength + 3; - unescapedPosition += copyLength + 2; - } - - int remainingLength = unescapedData.length - unescapedPosition; - System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, remainingLength); - return unescapedData; - } - - private int findNextUnescapeIndex(byte[] bytes, int offset, int limit) { - for (int i = offset; i < limit - 2; i++) { - if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) { - return i; - } - } - return limit; - } - - } - - /** - * Parses a SEI data from H.264 frames and extracts samples with closed captions data. - * - * TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that - * a sample with an earlier timestamp won't be added to it. - */ - private class SeiReader extends SampleQueue implements Comparator { - - // SEI data, used for Closed Captions. - private static final int NAL_UNIT_TYPE_SEI = 6; - - private final BitArray seiBuffer; - private final TreeSet internalQueue; - - public SeiReader(SamplePool samplePool) { - super(samplePool); - setMediaFormat(MediaFormat.createEia608Format()); - seiBuffer = new BitArray(); - internalQueue = new TreeSet(this); - } - - @SuppressLint("InlinedApi") - public void read(byte[] data, int size, long pesTimeUs) { - seiBuffer.reset(data, size); - while (seiBuffer.bytesLeft() > 0) { - int seiStart = seiBuffer.findNextNalUnit(NAL_UNIT_TYPE_SEI, 0); - if (seiStart == seiBuffer.bytesLeft()) { - return; - } - seiBuffer.skipBytes(seiStart + 4); - int ccDataSize = Eia608Parser.parseHeader(seiBuffer); - if (ccDataSize > 0) { - addSample(Sample.TYPE_MISC, seiBuffer, ccDataSize, pesTimeUs, true); - } - } - } - - @Override - public int compare(Sample first, Sample second) { - // Note - We don't expect samples to have identical timestamps. - return first.timeUs <= second.timeUs ? -1 : 1; - } - - @Override - protected synchronized Sample internalPeekSample() { - return internalQueue.isEmpty() ? null : internalQueue.first(); - } - - @Override - protected synchronized Sample internalPollSample() { - return internalQueue.pollFirst(); - } - - @Override - protected synchronized void internalQueueSample(Sample sample) { - internalQueue.add(sample); - } - - } - - /** - * Parses a continuous ADTS byte stream and extracts individual frames. - */ - private class AdtsReader extends PesPayloadReader { - - private final BitArray adtsBuffer; - private long timeUs; - private long frameDurationUs; - - public AdtsReader(SamplePool samplePool) { - super(samplePool); - adtsBuffer = new BitArray(); - } - - @Override - public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { - boolean needToProcessLeftOvers = !adtsBuffer.isEmpty(); - adtsBuffer.append(pesBuffer, pesPayloadSize); - // If there are leftovers from previous PES packet, process it with last calculated timeUs. - if (needToProcessLeftOvers && !readOneAacFrame(timeUs)) { - return; - } - int frameIndex = 0; - do { - timeUs = pesTimeUs + (frameDurationUs * frameIndex++); - } while(readOneAacFrame(timeUs)); - } - - @SuppressLint("InlinedApi") - private boolean readOneAacFrame(long timeUs) { - if (adtsBuffer.isEmpty()) { - return false; - } - - int offsetToSyncWord = adtsBuffer.findNextAdtsSyncWord(); - adtsBuffer.skipBytes(offsetToSyncWord); - - int adtsStartOffset = adtsBuffer.getByteOffset(); - - if (adtsBuffer.bytesLeft() < 7) { - adtsBuffer.setByteOffset(adtsStartOffset); - adtsBuffer.clearReadData(); - return false; - } - - adtsBuffer.skipBits(15); - boolean hasCRC = !adtsBuffer.readBit(); - - if (!hasMediaFormat()) { - int audioObjectType = adtsBuffer.readBits(2) + 1; - int sampleRateIndex = adtsBuffer.readBits(4); - adtsBuffer.skipBits(1); - int channelConfig = adtsBuffer.readBits(3); - - byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAudioSpecificConfig( - audioObjectType, sampleRateIndex, channelConfig); - Pair audioParams = CodecSpecificDataUtil.parseAudioSpecificConfig( - audioSpecificConfig); - - MediaFormat mediaFormat = MediaFormat.createAudioFormat(MimeTypes.AUDIO_AAC, - MediaFormat.NO_VALUE, audioParams.second, audioParams.first, - Collections.singletonList(audioSpecificConfig)); - frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate; - setMediaFormat(mediaFormat); - } else { - adtsBuffer.skipBits(10); - } - - adtsBuffer.skipBits(4); - int frameSize = adtsBuffer.readBits(13); - adtsBuffer.skipBits(13); - - // Decrement frame size by ADTS header size and CRC. - if (hasCRC) { - // Skip CRC. - adtsBuffer.skipBytes(2); - frameSize -= 9; - } else { - frameSize -= 7; - } - - if (frameSize > adtsBuffer.bytesLeft()) { - adtsBuffer.setByteOffset(adtsStartOffset); - adtsBuffer.clearReadData(); - return false; - } - - addSample(Sample.TYPE_AUDIO, adtsBuffer, frameSize, timeUs, true); - return true; - } - - @Override - public void release() { - super.release(); - adtsBuffer.reset(); - } - - } - - /** - * Parses ID3 data and extracts individual text information frames. - */ - private class Id3Reader extends PesPayloadReader { - - public Id3Reader(SamplePool samplePool) { - super(samplePool); - setMediaFormat(MediaFormat.createId3Format()); - } - - @SuppressLint("InlinedApi") - @Override - public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { - addSample(Sample.TYPE_MISC, pesBuffer, pesPayloadSize, pesTimeUs, true); - } - - } - - /** - * A pool from which the extractor can obtain sample objects for internal use. - * - * TODO: Over time the average size of a sample in the video pool will become larger, as the - * proportion of samples in the pool that have at some point held a keyframe grows. Currently - * this leads to inefficient memory usage, since samples large enough to hold keyframes end up - * being used to hold non-keyframes. We need to fix this. - */ - public static class SamplePool { - - private static final int[] DEFAULT_SAMPLE_SIZES; - static { - DEFAULT_SAMPLE_SIZES = new int[Sample.TYPE_COUNT]; - DEFAULT_SAMPLE_SIZES[Sample.TYPE_VIDEO] = 10 * 1024; - DEFAULT_SAMPLE_SIZES[Sample.TYPE_AUDIO] = 512; - DEFAULT_SAMPLE_SIZES[Sample.TYPE_MISC] = 512; - } - - private final Sample[] pools; - - public SamplePool() { - pools = new Sample[Sample.TYPE_COUNT]; - } - - /* package */ synchronized Sample get(int type) { - if (pools[type] == null) { - return new Sample(type, DEFAULT_SAMPLE_SIZES[type]); - } - Sample sample = pools[type]; - pools[type] = sample.nextInPool; - sample.nextInPool = null; - return sample; - } - - /* package */ synchronized void recycle(Sample sample) { - sample.reset(); - sample.nextInPool = pools[sample.type]; - pools[sample.type] = sample; - } - - } - - /** - * An internal variant of {@link SampleHolder} for internal pooling and buffering. - */ - private static class Sample { - - public static final int TYPE_VIDEO = 0; - public static final int TYPE_AUDIO = 1; - public static final int TYPE_MISC = 2; - public static final int TYPE_COUNT = 3; - - public final int type; - public Sample nextInPool; - - public byte[] data; - public boolean isKeyframe; - public int size; - public long timeUs; - - public Sample(int type, int length) { - this.type = type; - data = new byte[length]; - } - - public void expand(int length) { - byte[] newBuffer = new byte[data.length + length]; - System.arraycopy(data, 0, newBuffer, 0, size); - data = newBuffer; - } - - public void reset() { - isKeyframe = false; - size = 0; - timeUs = 0; - } - - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java new file mode 100644 index 0000000000..35813052ad --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java @@ -0,0 +1,180 @@ +/* + * 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 audioParams = CodecSpecificDataUtil.parseAudioSpecificConfig( + audioSpecificConfig); + + MediaFormat mediaFormat = MediaFormat.createAudioFormat(MimeTypes.AUDIO_AAC, + MediaFormat.NO_VALUE, audioParams.second, audioParams.first, + Collections.singletonList(audioSpecificConfig)); + frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate; + setMediaFormat(mediaFormat); + } else { + adtsScratch.skipBits(10); + } + + adtsScratch.skipBits(4); + sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE; + if (hasCrc) { + sampleSize -= CRC_SIZE; + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java new file mode 100644 index 0000000000..55faeefcf4 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.hls.parser; + +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.mp4.Mp4Util; +import com.google.android.exoplayer.upstream.BufferPool; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Parses a continuous H264 byte stream and extracts individual frames. + */ +/* package */ class H264Reader extends PesPayloadReader { + + private static final int NAL_UNIT_TYPE_IDR = 5; + private static final int NAL_UNIT_TYPE_SEI = 6; + private static final int NAL_UNIT_TYPE_SPS = 7; + private static final int NAL_UNIT_TYPE_PPS = 8; + private static final int NAL_UNIT_TYPE_AUD = 9; + + private final SeiReader seiReader; + private final boolean[] prefixFlags; + private final NalUnitTargetBuffer sps; + private final NalUnitTargetBuffer pps; + private final NalUnitTargetBuffer sei; + + private boolean isKeyframe; + + public H264Reader(BufferPool bufferPool, SeiReader seiReader) { + super(bufferPool); + this.seiReader = seiReader; + prefixFlags = new boolean[3]; + sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); + pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); + sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); + } + + @Override + public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { + while (data.bytesLeft() > 0) { + int offset = data.getPosition(); + int limit = data.limit(); + byte[] dataArray = data.data; + + // Append the data to the buffer. + appendData(data, data.bytesLeft()); + + // Scan the appended data, processing NAL units as they are encountered + while (offset < limit) { + int nextNalUnitOffset = Mp4Util.findNalUnit(dataArray, offset, limit, prefixFlags); + if (nextNalUnitOffset < limit) { + // We've seen the start of a NAL unit. + + // This is the length to the start of the unit. It may be negative if the NAL unit + // actually started in previously consumed data. + int lengthToNalUnit = nextNalUnitOffset - offset; + if (lengthToNalUnit > 0) { + feedNalUnitTargetBuffersData(dataArray, offset, nextNalUnitOffset); + } + + int nalUnitType = Mp4Util.getNalUnitType(dataArray, nextNalUnitOffset); + int nalUnitOffsetInData = nextNalUnitOffset - limit; + if (nalUnitType == NAL_UNIT_TYPE_AUD) { + if (writingSample()) { + if (isKeyframe && !hasMediaFormat() && sps.isCompleted() && pps.isCompleted()) { + parseMediaFormat(sps, pps); + } + commitSample(isKeyframe, nalUnitOffsetInData); + } + startSample(pesTimeUs, nalUnitOffsetInData); + isKeyframe = false; + } else if (nalUnitType == NAL_UNIT_TYPE_IDR) { + isKeyframe = true; + } + + // If the length to the start of the unit is negative then we wrote too many bytes to the + // NAL buffers. Discard the excess bytes when notifying that the unit has ended. + feedNalUnitTargetEnd(pesTimeUs, lengthToNalUnit < 0 ? -lengthToNalUnit : 0); + // Notify the start of the next NAL unit. + feedNalUnitTargetBuffersStart(nalUnitType); + // Continue scanning the data. + offset = nextNalUnitOffset + 4; + } else { + feedNalUnitTargetBuffersData(dataArray, offset, limit); + offset = limit; + } + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + private void feedNalUnitTargetBuffersStart(int nalUnitType) { + if (!hasMediaFormat()) { + sps.startNalUnit(nalUnitType); + pps.startNalUnit(nalUnitType); + } + sei.startNalUnit(nalUnitType); + } + + private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) { + if (!hasMediaFormat()) { + sps.appendToNalUnit(dataArray, offset, limit); + pps.appendToNalUnit(dataArray, offset, limit); + } + sei.appendToNalUnit(dataArray, offset, limit); + } + + private void feedNalUnitTargetEnd(long pesTimeUs, int discardPadding) { + sps.endNalUnit(discardPadding); + pps.endNalUnit(discardPadding); + if (sei.endNalUnit(discardPadding)) { + seiReader.read(sei.nalData, 0, pesTimeUs); + } + } + + private void parseMediaFormat(NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { + byte[] spsData = new byte[sps.nalLength]; + byte[] ppsData = new byte[pps.nalLength]; + System.arraycopy(sps.nalData, 0, spsData, 0, sps.nalLength); + System.arraycopy(pps.nalData, 0, ppsData, 0, pps.nalLength); + List initializationData = new ArrayList(); + initializationData.add(spsData); + initializationData.add(ppsData); + + // Unescape and then parse the SPS unit. + byte[] unescapedSps = unescapeStream(spsData, 0, spsData.length); + ParsableBitArray bitArray = new ParsableBitArray(unescapedSps); + bitArray.skipBits(32); // NAL header + int profileIdc = bitArray.readBits(8); + bitArray.skipBits(16); // constraint bits (6), reserved (2) and level_idc (8) + bitArray.readUnsignedExpGolombCodedInt(); // seq_parameter_set_id + + int chromaFormatIdc = 1; // Default is 4:2:0 + if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244 + || profileIdc == 44 || profileIdc == 83 || profileIdc == 86 || profileIdc == 118 + || profileIdc == 128 || profileIdc == 138) { + chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); + if (chromaFormatIdc == 3) { + bitArray.skipBits(1); // separate_colour_plane_flag + } + bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8 + bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8 + bitArray.skipBits(1); // qpprime_y_zero_transform_bypass_flag + boolean seqScalingMatrixPresentFlag = bitArray.readBit(); + if (seqScalingMatrixPresentFlag) { + int limit = (chromaFormatIdc != 3) ? 8 : 12; + for (int i = 0; i < limit; i++) { + boolean seqScalingListPresentFlag = bitArray.readBit(); + if (seqScalingListPresentFlag) { + skipScalingList(bitArray, i < 6 ? 16 : 64); + } + } + } + } + + bitArray.readUnsignedExpGolombCodedInt(); // log2_max_frame_num_minus4 + long picOrderCntType = bitArray.readUnsignedExpGolombCodedInt(); + if (picOrderCntType == 0) { + bitArray.readUnsignedExpGolombCodedInt(); // log2_max_pic_order_cnt_lsb_minus4 + } else if (picOrderCntType == 1) { + bitArray.skipBits(1); // delta_pic_order_always_zero_flag + bitArray.readSignedExpGolombCodedInt(); // offset_for_non_ref_pic + bitArray.readSignedExpGolombCodedInt(); // offset_for_top_to_bottom_field + long numRefFramesInPicOrderCntCycle = bitArray.readUnsignedExpGolombCodedInt(); + for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) { + bitArray.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i] + } + } + bitArray.readUnsignedExpGolombCodedInt(); // max_num_ref_frames + bitArray.skipBits(1); // gaps_in_frame_num_value_allowed_flag + + int picWidthInMbs = bitArray.readUnsignedExpGolombCodedInt() + 1; + int picHeightInMapUnits = bitArray.readUnsignedExpGolombCodedInt() + 1; + boolean frameMbsOnlyFlag = bitArray.readBit(); + int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits; + if (!frameMbsOnlyFlag) { + bitArray.skipBits(1); // mb_adaptive_frame_field_flag + } + + bitArray.skipBits(1); // direct_8x8_inference_flag + int frameWidth = picWidthInMbs * 16; + int frameHeight = frameHeightInMbs * 16; + boolean frameCroppingFlag = bitArray.readBit(); + if (frameCroppingFlag) { + int frameCropLeftOffset = bitArray.readUnsignedExpGolombCodedInt(); + int frameCropRightOffset = bitArray.readUnsignedExpGolombCodedInt(); + int frameCropTopOffset = bitArray.readUnsignedExpGolombCodedInt(); + int frameCropBottomOffset = bitArray.readUnsignedExpGolombCodedInt(); + int cropUnitX, cropUnitY; + if (chromaFormatIdc == 0) { + cropUnitX = 1; + cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0); + } else { + int subWidthC = (chromaFormatIdc == 3) ? 1 : 2; + int subHeightC = (chromaFormatIdc == 1) ? 2 : 1; + cropUnitX = subWidthC; + cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0)); + } + frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX; + frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY; + } + + // Set the format. + setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, + frameWidth, frameHeight, initializationData)); + } + + private void skipScalingList(ParsableBitArray bitArray, int size) { + int lastScale = 8; + int nextScale = 8; + for (int i = 0; i < size; i++) { + if (nextScale != 0) { + int deltaScale = bitArray.readSignedExpGolombCodedInt(); + nextScale = (lastScale + deltaScale + 256) % 256; + } + lastScale = (nextScale == 0) ? lastScale : nextScale; + } + } + + /** + * Replaces occurrences of [0, 0, 3] with [0, 0]. + *

+ * See ISO/IEC 14496-10:2005(E) page 36 for more information. + */ + private byte[] unescapeStream(byte[] data, int offset, int limit) { + int position = offset; + List escapePositions = new ArrayList(); + while (position < limit) { + position = findNextUnescapeIndex(data, position, limit); + if (position < limit) { + escapePositions.add(position); + position += 3; + } + } + + int escapeCount = escapePositions.size(); + int escapedPosition = offset; // The position being read from. + int unescapedPosition = 0; // The position being written to. + byte[] unescapedData = new byte[limit - offset - escapeCount]; + for (int i = 0; i < escapeCount; i++) { + int nextEscapePosition = escapePositions.get(i); + int copyLength = nextEscapePosition - escapedPosition; + System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, copyLength); + escapedPosition += copyLength + 3; + unescapedPosition += copyLength + 2; + } + + int remainingLength = unescapedData.length - unescapedPosition; + System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, remainingLength); + return unescapedData; + } + + private int findNextUnescapeIndex(byte[] bytes, int offset, int limit) { + for (int i = offset; i < limit - 2; i++) { + if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) { + return i; + } + } + return limit; + } + + /** + * A buffer that fills itself with data corresponding to a specific NAL unit, as it is + * encountered in the stream. + */ + private static final class NalUnitTargetBuffer { + + private final int targetType; + + private boolean isFilling; + private boolean isCompleted; + + public byte[] nalData; + public int nalLength; + + public NalUnitTargetBuffer(int targetType, int initialCapacity) { + this.targetType = targetType; + // Initialize data, writing the known NAL prefix into the first four bytes. + nalData = new byte[4 + initialCapacity]; + nalData[2] = 1; + nalData[3] = (byte) targetType; + } + + public boolean isCompleted() { + return isCompleted; + } + + /** + * Invoked to indicate that a NAL unit has started. + * + * @param type The type of the NAL unit. + */ + public void startNalUnit(int type) { + Assertions.checkState(!isFilling); + isFilling = type == targetType; + if (isFilling) { + // Length is initially the length of the NAL prefix. + nalLength = 4; + isCompleted = false; + } + } + + /** + * Invoked to pass stream data. The data passed should not include 4 byte NAL unit prefixes. + * + * @param data Holds the data being passed. + * @param offset The offset of the data in {@code data}. + * @param limit The limit (exclusive) of the data in {@code data}. + */ + public void appendToNalUnit(byte[] data, int offset, int limit) { + if (!isFilling) { + return; + } + int readLength = limit - offset; + if (nalData.length < nalLength + readLength) { + nalData = Arrays.copyOf(nalData, (nalLength + readLength) * 2); + } + System.arraycopy(data, offset, nalData, nalLength, readLength); + nalLength += readLength; + } + + /** + * Invoked to indicate that a NAL unit has ended. + * + * @param discardPadding The number of excess bytes that were passed to + * {@link #appendToNalUnit(byte[], int, int)}, which should be discarded. + * @return True if the ended NAL unit is of the target type. False otherwise. + */ + public boolean endNalUnit(int discardPadding) { + if (!isFilling) { + return false; + } + nalLength -= discardPadding; + isFilling = false; + isCompleted = true; + return true; + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java new file mode 100644 index 0000000000..609337b664 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java @@ -0,0 +1,47 @@ +/* + * 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); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java new file mode 100644 index 0000000000..2bdce8448a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java @@ -0,0 +1,47 @@ +/* + * 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(); + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/RollingSampleBuffer.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/RollingSampleBuffer.java new file mode 100644 index 0000000000..48a618edde --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/RollingSampleBuffer.java @@ -0,0 +1,303 @@ +/* + * 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.SampleHolder; +import com.google.android.exoplayer.upstream.BufferPool; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * A rolling buffer of sample data and corresponding sample information. + */ +/* package */ final class RollingSampleBuffer { + + private final BufferPool fragmentPool; + private final int fragmentLength; + + private final InfoQueue infoQueue; + private final ConcurrentLinkedQueue dataQueue; + private final long[] dataOffsetHolder; + + // Accessed only by the consuming thread. + private long totalBytesDropped; + + // Accessed only by the loading thread. + private long totalBytesWritten; + private byte[] lastFragment; + private int lastFragmentOffset; + private long pendingSampleTimeUs; + private long pendingSampleOffset; + + public RollingSampleBuffer(BufferPool bufferPool) { + this.fragmentPool = bufferPool; + fragmentLength = bufferPool.bufferLength; + infoQueue = new InfoQueue(); + dataQueue = new ConcurrentLinkedQueue(); + dataOffsetHolder = new long[1]; + } + + public void release() { + while (!dataQueue.isEmpty()) { + fragmentPool.releaseDirect(dataQueue.remove()); + } + } + + // Called by the consuming thread. + + /** + * Fills {@code holder} with information about the current sample, but does not write its data. + *

+ * The fields set are {SampleHolder#size}, {SampleHolder#timeUs} and {SampleHolder#flags}. + * + * @param holder The holder into which the current sample information should be written. + * @return True if the holder was filled. False if there is no current sample. + */ + public boolean peekSample(SampleHolder holder) { + return infoQueue.peekSample(holder, dataOffsetHolder); + } + + /** + * Skips the current sample. + */ + public void skipSample() { + long nextOffset = infoQueue.moveToNextSample(); + dropFragmentsTo(nextOffset); + } + + /** + * Reads the current sample, advancing the read index to the next sample. + * + * @param holder The holder into which the current sample should be written. + */ + public void readSample(SampleHolder holder) { + // Write the sample information into the holder. + infoQueue.peekSample(holder, dataOffsetHolder); + // Write the sample data into the holder. + if (holder.data == null || holder.data.capacity() < holder.size) { + holder.replaceBuffer(holder.size); + } + if (holder.data != null) { + readData(dataOffsetHolder[0], holder.data, holder.size); + } + // Advance the read head. + long nextOffset = infoQueue.moveToNextSample(); + dropFragmentsTo(nextOffset); + } + + /** + * Reads data from the front of the rolling buffer. + * + * @param absolutePosition The absolute position from which data should be read. + * @param target The buffer into which data should be written. + * @param length The number of bytes to read. + */ + private void readData(long absolutePosition, ByteBuffer target, int length) { + int remaining = length; + while (remaining > 0) { + dropFragmentsTo(absolutePosition); + int positionInFragment = (int) (absolutePosition - totalBytesDropped); + int toCopy = Math.min(remaining, fragmentLength - positionInFragment); + target.put(dataQueue.peek(), positionInFragment, toCopy); + absolutePosition += toCopy; + remaining -= toCopy; + } + } + + /** + * Discard any fragments that hold data prior to the specified absolute position, returning + * them to the pool. + * + * @param absolutePosition The absolute position up to which fragments can be discarded. + */ + private void dropFragmentsTo(long absolutePosition) { + int relativePosition = (int) (absolutePosition - totalBytesDropped); + int fragmentIndex = relativePosition / fragmentLength; + for (int i = 0; i < fragmentIndex; i++) { + fragmentPool.releaseDirect(dataQueue.remove()); + totalBytesDropped += fragmentLength; + } + } + + // Called by the loading thread. + + /** + * Indicates the start point for the next sample. + * + * @param sampleTimeUs The sample timestamp. + * @param offset The offset of the sample's data, relative to the total number of bytes written + * to the buffer. Must be negative or zero. + */ + public void startSample(long sampleTimeUs, int offset) { + Assertions.checkState(offset <= 0); + pendingSampleTimeUs = sampleTimeUs; + pendingSampleOffset = totalBytesWritten + offset; + } + + /** + * Appends data to the rolling buffer. + * + * @param buffer A buffer containing the data to append. + * @param length The length of the data to append. + */ + public void appendData(ParsableByteArray buffer, int length) { + int remainingWriteLength = length; + while (remainingWriteLength > 0) { + if (dataQueue.isEmpty() || lastFragmentOffset == fragmentLength) { + lastFragmentOffset = 0; + lastFragment = fragmentPool.allocateDirect(); + dataQueue.add(lastFragment); + } + int thisWriteLength = Math.min(remainingWriteLength, fragmentLength - lastFragmentOffset); + buffer.readBytes(lastFragment, lastFragmentOffset, thisWriteLength); + lastFragmentOffset += thisWriteLength; + remainingWriteLength -= thisWriteLength; + } + totalBytesWritten += length; + } + + /** + * Indicates the end point for the current sample, making it available for consumption. + * + * @param isKeyframe True if the sample being committed is a keyframe. False otherwise. + * @param offset The offset of the first byte after the end of the sample's data, relative to + * the total number of bytes written to the buffer. Must be negative or zero. + */ + public void commitSample(boolean isKeyframe, int offset) { + Assertions.checkState(offset <= 0); + int sampleSize = (int) (totalBytesWritten + offset - pendingSampleOffset); + infoQueue.commitSample(pendingSampleTimeUs, pendingSampleOffset, sampleSize, + isKeyframe ? C.SAMPLE_FLAG_SYNC : 0); + } + + /** + * Holds information about the samples in the rolling buffer. + */ + private static class InfoQueue { + + private static final int SAMPLE_CAPACITY_INCREMENT = 1000; + + private int capacity; + + private long[] offsets; + private int[] sizes; + private int[] flags; + private long[] timesUs; + + private int queueSize; + private int readIndex; + private int writeIndex; + + public InfoQueue() { + capacity = SAMPLE_CAPACITY_INCREMENT; + offsets = new long[capacity]; + timesUs = new long[capacity]; + flags = new int[capacity]; + sizes = new int[capacity]; + } + + // Called by the consuming thread. + + /** + * Fills {@code holder} with information about the current sample, but does not write its data. + * The first entry in {@code offsetHolder} is filled with the absolute position of the sample's + * data in the rolling buffer. + *

+ * The fields set are {SampleHolder#size}, {SampleHolder#timeUs}, {SampleHolder#flags} and + * {@code offsetHolder[0]}. + * + * @param holder The holder into which the current sample information should be written. + * @param offsetHolder The holder into which the absolute position of the sample's data should + * be written. + * @return True if the holders were filled. False if there is no current sample. + */ + public synchronized boolean peekSample(SampleHolder holder, long[] offsetHolder) { + if (queueSize == 0) { + return false; + } + holder.timeUs = timesUs[readIndex]; + holder.size = sizes[readIndex]; + holder.flags = flags[readIndex]; + offsetHolder[0] = offsets[readIndex]; + return true; + } + + /** + * Advances the read index to the next sample. + * + * @return The absolute position of the first byte in the rolling buffer that may still be + * required after advancing the index. Data prior to this position can be dropped. + */ + public synchronized long moveToNextSample() { + queueSize--; + int lastReadIndex = readIndex++; + if (readIndex == capacity) { + // Wrap around. + readIndex = 0; + } + return queueSize > 0 ? offsets[readIndex] : (sizes[lastReadIndex] + offsets[lastReadIndex]); + } + + // Called by the loading thread. + + public synchronized void commitSample(long timeUs, long offset, int size, int sampleFlags) { + timesUs[writeIndex] = timeUs; + offsets[writeIndex] = offset; + sizes[writeIndex] = size; + flags[writeIndex] = sampleFlags; + // Increment the write index. + queueSize++; + if (queueSize == capacity) { + // Increase the capacity. + int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; + long[] newOffsets = new long[newCapacity]; + long[] newTimesUs = new long[newCapacity]; + int[] newFlags = new int[newCapacity]; + int[] newSizes = new int[newCapacity]; + int beforeWrap = capacity - readIndex; + System.arraycopy(offsets, readIndex, newOffsets, 0, beforeWrap); + System.arraycopy(timesUs, readIndex, newTimesUs, 0, beforeWrap); + System.arraycopy(flags, readIndex, newFlags, 0, beforeWrap); + System.arraycopy(sizes, readIndex, newSizes, 0, beforeWrap); + int afterWrap = readIndex; + System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); + System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); + System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); + System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); + offsets = newOffsets; + timesUs = newTimesUs; + flags = newFlags; + sizes = newSizes; + readIndex = 0; + writeIndex = capacity; + queueSize = capacity; + capacity = newCapacity; + } else { + writeIndex++; + if (writeIndex == capacity) { + // Wrap around. + writeIndex = 0; + } + } + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java new file mode 100644 index 0000000000..348a2cd843 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java @@ -0,0 +1,202 @@ +/* + * 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. + *

+ * 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; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java new file mode 100644 index 0000000000..6da719ae22 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java @@ -0,0 +1,50 @@ +/* + * 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); + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java new file mode 100644 index 0000000000..d7ad5e7dde --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java @@ -0,0 +1,608 @@ +/* + * 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.upstream.DataSource; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; + +import android.util.Log; +import android.util.SparseArray; + +import java.io.IOException; + +/** + * Facilitates the extraction of data from the MPEG-2 TS container format. + */ +public final class TsExtractor { + + private static final String TAG = "TsExtractor"; + + private static final int TS_PACKET_SIZE = 188; + private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. + private static final int TS_PAT_PID = 0; + + private static final int TS_STREAM_TYPE_AAC = 0x0F; + private static final int TS_STREAM_TYPE_H264 = 0x1B; + private static final int TS_STREAM_TYPE_ID3 = 0x15; + private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 + + private static final long MAX_PTS = 0x1FFFFFFFFL; + + private final ParsableByteArray tsPacketBuffer; + private final SparseArray sampleQueues; // Indexed by streamType + private final SparseArray tsPayloadReaders; // Indexed by pid + private final BufferPool bufferPool; + private final boolean shouldSpliceIn; + private final long firstSampleTimestamp; + private final ParsableBitArray tsScratch; + + // Accessed only by the consuming thread. + private boolean spliceConfigured; + + // Accessed only by the loading thread. + private int tsPacketBytesRead; + private long timestampOffsetUs; + private long lastPts; + + // Accessed by both the loading and consuming threads. + private volatile boolean prepared; + + public TsExtractor(long firstSampleTimestamp, boolean shouldSpliceIn, BufferPool bufferPool) { + this.firstSampleTimestamp = firstSampleTimestamp; + this.shouldSpliceIn = shouldSpliceIn; + this.bufferPool = bufferPool; + tsScratch = new ParsableBitArray(new byte[3]); + tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); + sampleQueues = new SparseArray(); + tsPayloadReaders = new SparseArray(); + tsPayloadReaders.put(TS_PAT_PID, new PatReader()); + lastPts = Long.MIN_VALUE; + } + + /** + * Gets the number of available tracks. + *

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

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

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

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

+ * Splice configuration may fail if the next extractor is not yet in a state that allows the + * splice to be performed. Calling this method is a noop if the splice has already been + * configured. Hence this method should be called repeatedly during the window within which a + * splice can be performed. + * + * @param nextExtractor The extractor being spliced to. + */ + public void configureSpliceTo(TsExtractor nextExtractor) { + Assertions.checkState(prepared); + if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) { + // The splice is already configured, or the next extractor doesn't want to be spliced in, or + // the next extractor isn't ready to be spliced in. + return; + } + boolean spliceConfigured = true; + for (int i = 0; i < sampleQueues.size(); i++) { + spliceConfigured &= sampleQueues.valueAt(i).configureSpliceTo( + nextExtractor.sampleQueues.valueAt(i)); + } + this.spliceConfigured = spliceConfigured; + return; + } + + /** + * Gets the largest timestamp of any sample parsed by the extractor. + * + * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed. + */ + public long getLargestSampleTimestamp() { + long largestParsedTimestampUs = Long.MIN_VALUE; + for (int i = 0; i < sampleQueues.size(); i++) { + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, + sampleQueues.valueAt(i).getLargestParsedTimestampUs()); + } + return largestParsedTimestampUs; + } + + /** + * Gets the next sample for the specified track. + * + * @param track The track from which to read. + * @param holder A {@link SampleHolder} into which the sample should be read. + * @return True if a sample was read. False otherwise. + */ + public boolean getSample(int track, SampleHolder holder) { + Assertions.checkState(prepared); + return sampleQueues.valueAt(track).getSample(holder); + } + + /** + * Discards samples for the specified track up to the specified time. + * + * @param track The track from which samples should be discarded. + * @param timeUs The time up to which samples should be discarded, in microseconds. + */ + public void discardUntil(int track, long timeUs) { + Assertions.checkState(prepared); + sampleQueues.valueAt(track).discardUntil(timeUs); + } + + /** + * Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the + * specified track. + * + * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)} + * for the specified track. False otherwise. + */ + public boolean hasSamples(int track) { + Assertions.checkState(prepared); + return !sampleQueues.valueAt(track).isEmpty(); + } + + private boolean checkPrepared() { + int pesPayloadReaderCount = sampleQueues.size(); + if (pesPayloadReaderCount == 0) { + return false; + } + for (int i = 0; i < pesPayloadReaderCount; i++) { + if (!sampleQueues.valueAt(i).hasMediaFormat()) { + return false; + } + } + return true; + } + + /** + * Reads up to a single TS packet. + * + * @param dataSource The {@link DataSource} from which to read. + * @throws IOException If an error occurred reading from the source. + * @return The number of bytes read from the source. + */ + public int read(DataSource dataSource) throws IOException { + int bytesRead = dataSource.read(tsPacketBuffer.data, tsPacketBytesRead, + TS_PACKET_SIZE - tsPacketBytesRead); + if (bytesRead == -1) { + return -1; + } + + tsPacketBytesRead += bytesRead; + if (tsPacketBytesRead < TS_PACKET_SIZE) { + // We haven't read the whole packet yet. + return bytesRead; + } + + // Reset before reading the packet. + tsPacketBytesRead = 0; + tsPacketBuffer.setPosition(0); + tsPacketBuffer.setLimit(TS_PACKET_SIZE); + + int syncByte = tsPacketBuffer.readUnsignedByte(); + if (syncByte != TS_SYNC_BYTE) { + return bytesRead; + } + + tsPacketBuffer.readBytes(tsScratch, 3); + tsScratch.skipBits(1); // transport_error_indicator + boolean payloadUnitStartIndicator = tsScratch.readBit(); + tsScratch.skipBits(1); // transport_priority + int pid = tsScratch.readBits(13); + tsScratch.skipBits(2); // transport_scrambling_control + boolean adaptationFieldExists = tsScratch.readBit(); + boolean payloadExists = tsScratch.readBit(); + // Last 4 bits of scratch are skipped: continuity_counter + + // Skip the adaptation field. + if (adaptationFieldExists) { + int adaptationFieldLength = tsPacketBuffer.readUnsignedByte(); + tsPacketBuffer.skip(adaptationFieldLength); + } + + // Read the payload. + if (payloadExists) { + TsPayloadReader payloadReader = tsPayloadReaders.get(pid); + if (payloadReader != null) { + payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); + } + } + + if (!prepared) { + prepared = checkPrepared(); + } + + return bytesRead; + } + + /** + * Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound. + * + * @param pts The raw PTS value. + * @return The corresponding time in microseconds. + */ + /* package */ long ptsToTimeUs(long pts) { + if (lastPts != Long.MIN_VALUE) { + // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), + // and we need to snap to the one closest to lastPts. + long closestWrapCount = (lastPts + (MAX_PTS / 2)) / MAX_PTS; + long ptsWrapBelow = pts + (MAX_PTS * (closestWrapCount - 1)); + long ptsWrapAbove = pts + (MAX_PTS * closestWrapCount); + pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts) + ? ptsWrapBelow : ptsWrapAbove; + } + // Calculate the corresponding timestamp. + long timeUs = (pts * C.MICROS_PER_SECOND) / 90000; + // If we haven't done the initial timestamp adjustment, do it now. + if (lastPts == Long.MIN_VALUE) { + timestampOffsetUs = firstSampleTimestamp - timeUs; + } + // Record the adjusted PTS to adjust for wraparound next time. + lastPts = pts; + return timeUs + timestampOffsetUs; + } + + /** + * Parses TS packet payload data. + */ + private abstract static class TsPayloadReader { + + public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator); + + } + + /** + * Parses Program Association Table data. + */ + private class PatReader extends TsPayloadReader { + + private final ParsableBitArray patScratch; + + public PatReader() { + patScratch = new ParsableBitArray(new byte[4]); + } + + @Override + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + // Skip pointer. + if (payloadUnitStartIndicator) { + int pointerField = data.readUnsignedByte(); + data.skip(pointerField); + } + + data.readBytes(patScratch, 3); + patScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) + int sectionLength = patScratch.readBits(12); + // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1), + // section_number (8), last_section_number (8) + data.skip(5); + + int programCount = (sectionLength - 9) / 4; + for (int i = 0; i < programCount; i++) { + data.readBytes(patScratch, 4); + patScratch.skipBits(19); // program_number (16), reserved (3) + int pid = patScratch.readBits(13); + tsPayloadReaders.put(pid, new PmtReader()); + } + + // Skip CRC_32. + } + + } + + /** + * Parses Program Map Table. + */ + private class PmtReader extends TsPayloadReader { + + private final ParsableBitArray pmtScratch; + + public PmtReader() { + pmtScratch = new ParsableBitArray(new byte[5]); + } + + @Override + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + // Skip pointer. + if (payloadUnitStartIndicator) { + int pointerField = data.readUnsignedByte(); + data.skip(pointerField); + } + + data.readBytes(pmtScratch, 3); + pmtScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) + int sectionLength = pmtScratch.readBits(12); + + // program_number (16), reserved (2), version_number (5), current_next_indicator (1), + // section_number (8), last_section_number (8), reserved (3), PCR_PID (13) + // Skip the rest of the PMT header. + data.skip(7); + + data.readBytes(pmtScratch, 2); + pmtScratch.skipBits(4); + int programInfoLength = pmtScratch.readBits(12); + + // Skip the descriptors. + data.skip(programInfoLength); + + int entriesSize = sectionLength - 9 /* Size of the rest of the fields before descriptors */ + - programInfoLength - 4 /* CRC size */; + while (entriesSize > 0) { + data.readBytes(pmtScratch, 5); + int streamType = pmtScratch.readBits(8); + pmtScratch.skipBits(3); // reserved + int elementaryPid = pmtScratch.readBits(13); + pmtScratch.skipBits(4); // reserved + int esInfoLength = pmtScratch.readBits(12); + + // Skip the descriptors. + data.skip(esInfoLength); + entriesSize -= esInfoLength + 5; + + if (sampleQueues.get(streamType) != null) { + continue; + } + + PesPayloadReader pesPayloadReader = null; + switch (streamType) { + case TS_STREAM_TYPE_AAC: + pesPayloadReader = new AdtsReader(bufferPool); + break; + case TS_STREAM_TYPE_H264: + SeiReader seiReader = new SeiReader(bufferPool); + sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader); + pesPayloadReader = new H264Reader(bufferPool, seiReader); + break; + case TS_STREAM_TYPE_ID3: + pesPayloadReader = new Id3Reader(bufferPool); + break; + } + + if (pesPayloadReader != null) { + sampleQueues.put(streamType, pesPayloadReader); + tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader)); + } + } + + // Skip CRC_32. + } + + } + + /** + * Parses PES packet data and extracts samples. + */ + private class PesReader extends TsPayloadReader { + + private static final int STATE_FINDING_HEADER = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_HEADER_EXTENSION = 2; + private static final int STATE_READING_BODY = 3; + + private static final int HEADER_SIZE = 9; + private static final int MAX_HEADER_EXTENSION_SIZE = 5; + + private final ParsableBitArray pesScratch; + private final PesPayloadReader pesPayloadReader; + + private int state; + private int bytesRead; + private boolean bodyStarted; + + private boolean ptsFlag; + private int extendedHeaderLength; + + private int payloadSize; + + private long timeUs; + + public PesReader(PesPayloadReader pesPayloadReader) { + this.pesPayloadReader = pesPayloadReader; + pesScratch = new ParsableBitArray(new byte[HEADER_SIZE]); + state = STATE_FINDING_HEADER; + } + + @Override + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + if (payloadUnitStartIndicator) { + switch (state) { + case STATE_FINDING_HEADER: + case STATE_READING_HEADER: + // Expected. + break; + case STATE_READING_HEADER_EXTENSION: + Log.w(TAG, "Unexpected start indicator reading extended header"); + break; + case STATE_READING_BODY: + // If payloadSize == -1 then the length of the previous packet was unspecified, and so + // we only know that it's finished now that we've seen the start of the next one. This + // is expected. If payloadSize != -1, then the length of the previous packet was known, + // but we didn't receive that amount of data. This is not expected. + if (payloadSize != -1) { + Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes"); + } + // Either way, if the body was started, notify the reader that it has now finished. + if (bodyStarted) { + pesPayloadReader.packetFinished(); + } + break; + } + setState(STATE_READING_HEADER); + } + + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_HEADER: + data.skip(data.bytesLeft()); + break; + case STATE_READING_HEADER: + if (continueRead(data, pesScratch.getData(), HEADER_SIZE)) { + setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER); + } + break; + case STATE_READING_HEADER_EXTENSION: + int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); + // Read as much of the extended header as we're interested in, and skip the rest. + if (continueRead(data, pesScratch.getData(), readLength) + && continueRead(data, null, extendedHeaderLength)) { + parseHeaderExtension(); + bodyStarted = false; + setState(STATE_READING_BODY); + } + break; + case STATE_READING_BODY: + readLength = data.bytesLeft(); + int padding = payloadSize == -1 ? 0 : readLength - payloadSize; + if (padding > 0) { + readLength -= padding; + data.setLimit(data.getPosition() + readLength); + } + pesPayloadReader.consume(data, timeUs, !bodyStarted); + bodyStarted = true; + if (payloadSize != -1) { + payloadSize -= readLength; + if (payloadSize == 0) { + pesPayloadReader.packetFinished(); + setState(STATE_READING_HEADER); + } + } + break; + } + } + } + + private void setState(int state) { + this.state = state; + bytesRead = 0; + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read, or {@code null} to skip. + * @param targetLength The target length of the read. + * @return Whether the target length has been reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + if (bytesToRead <= 0) { + return true; + } else if (target == null) { + source.skip(bytesToRead); + } else { + source.readBytes(target, bytesRead, bytesToRead); + } + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + private boolean parseHeader() { + pesScratch.setPosition(0); + int startCodePrefix = pesScratch.readBits(24); + if (startCodePrefix != 0x000001) { + Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); + payloadSize = -1; + return false; + } + + pesScratch.skipBits(8); // stream_id. + int packetLength = pesScratch.readBits(16); + // First 8 bits are skipped: '10' (2), PES_scrambling_control (2), PES_priority (1), + // data_alignment_indicator (1), copyright (1), original_or_copy (1) + pesScratch.skipBits(8); + ptsFlag = pesScratch.readBit(); + // DTS_flag (1), ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1), + // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1) + pesScratch.skipBits(7); + extendedHeaderLength = pesScratch.readBits(8); + + if (packetLength == 0) { + payloadSize = -1; + } else { + payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */ + - HEADER_SIZE - extendedHeaderLength; + } + return true; + } + + private void parseHeaderExtension() { + pesScratch.setPosition(0); + timeUs = 0; + if (ptsFlag) { + pesScratch.skipBits(4); // '0010' + long pts = pesScratch.readBitsLong(3) << 30; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBitsLong(15) << 15; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBitsLong(15); + pesScratch.skipBits(1); // marker_bit + timeUs = ptsToTimeUs(pts); + } + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index efa3b66147..1ec0e363dd 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer.metadata; import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableByteArray; import java.io.UnsupportedEncodingException; import java.util.Collections; @@ -37,30 +37,28 @@ public class Id3Parser implements MetadataParser> { @Override public Map parse(byte[] data, int size) throws UnsupportedEncodingException, ParserException { - BitArray id3Buffer = new BitArray(data, size); - int id3Size = parseId3Header(id3Buffer); - Map metadata = new HashMap(); + ParsableByteArray id3Data = new ParsableByteArray(data, size); + int id3Size = parseId3Header(id3Data); while (id3Size > 0) { - int frameId0 = id3Buffer.readUnsignedByte(); - int frameId1 = id3Buffer.readUnsignedByte(); - int frameId2 = id3Buffer.readUnsignedByte(); - int frameId3 = id3Buffer.readUnsignedByte(); - - int frameSize = id3Buffer.readSynchSafeInt(); + int frameId0 = id3Data.readUnsignedByte(); + int frameId1 = id3Data.readUnsignedByte(); + int frameId2 = id3Data.readUnsignedByte(); + int frameId3 = id3Data.readUnsignedByte(); + int frameSize = id3Data.readSynchSafeInt(); if (frameSize <= 1) { break; } - id3Buffer.skipBytes(2); // Skip frame flags. - + // Skip frame flags. + id3Data.skip(2); // Check Frame ID == TXXX. if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { - int encoding = id3Buffer.readUnsignedByte(); + int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); byte[] frame = new byte[frameSize - 1]; - id3Buffer.readBytes(frame, 0, frameSize - 1); + id3Data.readBytes(frame, 0, frameSize - 1); int firstZeroIndex = indexOf(frame, 0, (byte) 0); String description = new String(frame, 0, firstZeroIndex, charset); @@ -72,7 +70,7 @@ public class Id3Parser implements MetadataParser> { } else { String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3); byte[] frame = new byte[frameSize]; - id3Buffer.readBytes(frame, 0, frameSize); + id3Data.readBytes(frame, 0, frameSize); metadata.put(type, frame); } @@ -101,12 +99,13 @@ public class Id3Parser implements MetadataParser> { } /** - * Parses ID3 header. - * @param id3Buffer A {@link BitArray} with raw ID3 data. - * @return The size of data that contains ID3 frames without header and footer. + * Parses an ID3 header. + * + * @param id3Buffer A {@link ParsableByteArray} from which data should be read. + * @return The size of ID3 frames in bytes, excluding the header and footer. * @throws ParserException If ID3 file identifier != "ID3". */ - private static int parseId3Header(BitArray id3Buffer) throws ParserException { + private static int parseId3Header(ParsableByteArray id3Buffer) throws ParserException { int id1 = id3Buffer.readUnsignedByte(); int id2 = id3Buffer.readUnsignedByte(); int id3 = id3Buffer.readUnsignedByte(); @@ -114,7 +113,7 @@ public class Id3Parser implements MetadataParser> { throw new ParserException(String.format( "Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); } - id3Buffer.skipBytes(2); // Skip version. + id3Buffer.skip(2); // Skip version. int flags = id3Buffer.readUnsignedByte(); int id3Size = id3Buffer.readSynchSafeInt(); @@ -123,7 +122,7 @@ public class Id3Parser implements MetadataParser> { if ((flags & 0x2) != 0) { int extendedHeaderSize = id3Buffer.readSynchSafeInt(); if (extendedHeaderSize > 4) { - id3Buffer.skipBytes(extendedHeaderSize - 4); + id3Buffer.skip(extendedHeaderSize - 4); } id3Size -= extendedHeaderSize; } diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java index 22e5b1edcb..4443e573c9 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java @@ -24,8 +24,6 @@ import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.Util; -import android.annotation.SuppressLint; -import android.media.MediaExtractor; import android.util.Pair; import java.util.ArrayList; @@ -37,8 +35,8 @@ public final class CommonMp4AtomParsers { /** Channel counts for AC-3 audio, indexed by acmod. (See ETSI TS 102 366.) */ private static final int[] AC3_CHANNEL_COUNTS = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; - /** Nominal bit-rates for AC-3 audio in kbps, indexed by bit_rate_code. (See ETSI TS 102 366.) */ - private static final int[] AC3_BIT_RATES = new int[] {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, + /** Nominal bitrates for AC-3 audio in kbps, indexed by bit_rate_code. (See ETSI TS 102 366.) */ + private static final int[] AC3_BITRATES = new int[] {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640}; /** @@ -81,7 +79,6 @@ public final class CommonMp4AtomParsers { * @param stblAtom stbl (sample table) atom to parse. * @return Sample table described by the stbl atom. */ - @SuppressLint("InlinedApi") public static Mp4TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom) { // Array of sample sizes. ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data; @@ -174,9 +171,9 @@ public final class CommonMp4AtomParsers { timestamps[i] = timestampTimeUnits + timestampOffset; // All samples are synchronization samples if the stss is not present. - flags[i] = stss == null ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; + flags[i] = stss == null ? C.SAMPLE_FLAG_SYNC : 0; if (i == nextSynchronizationSampleIndex) { - flags[i] = MediaExtractor.SAMPLE_FLAG_SYNC; + flags[i] = C.SAMPLE_FLAG_SYNC; remainingSynchronizationSamples--; if (remainingSynchronizationSamples > 0) { nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1; @@ -639,8 +636,8 @@ public final class CommonMp4AtomParsers { channelCount++; } - // Map bit_rate_code onto a bit-rate in kbit/s. - int bitrate = AC3_BIT_RATES[((nextByte & 0x03) << 3) + (parent.readUnsignedByte() >> 5)]; + // Map bit_rate_code onto a bitrate in kbit/s. + int bitrate = AC3_BITRATES[((nextByte & 0x03) << 3) + (parent.readUnsignedByte() >> 5)]; return new Ac3Format(channelCount, sampleRate, bitrate); } diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java index 325247edf1..9e60146140 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java @@ -15,11 +15,10 @@ */ package com.google.android.exoplayer.mp4; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; -import android.media.MediaExtractor; - /** Sample table for a track in an MP4 file. */ public final class Mp4TrackSampleTable { @@ -59,7 +58,7 @@ public final class Mp4TrackSampleTable { public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) { int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false); for (int i = startIndex; i >= 0; i--) { - if (timestampsUs[i] <= timeUs && (flags[i] & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) { + if (timestampsUs[i] <= timeUs && (flags[i] & C.SAMPLE_FLAG_SYNC) != 0) { return i; } } @@ -77,7 +76,7 @@ public final class Mp4TrackSampleTable { public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) { int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false); for (int i = startIndex; i < timestampsUs.length; i++) { - if (timestampsUs[i] >= timeUs && (flags[i] & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) { + if (timestampsUs[i] >= timeUs && (flags[i] & C.SAMPLE_FLAG_SYNC) != 0) { return i; } } diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java index a78c9fb414..7b744dfea6 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.mp4; +import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.ParsableByteArray; @@ -99,4 +100,155 @@ public final class Mp4Util { return CodecSpecificDataUtil.buildNalUnit(atom.data, offset, length); } + /** + * Finds the first NAL unit in {@code data}. + *

+ * For a NAL unit to be found, its first four bytes must be contained within the part of the + * array being searched. + * + * @param 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. + *

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

+ * Note that when using {@code prefixFlags} the return value may be 3, 2 or 1 less than + * {@code startOffset}, to indicate a NAL unit starting 3, 2 or 1 bytes before the first byte in + * the current array. + * + * @param data The data to search. + * @param startOffset The offset (inclusive) in the data to start the search. + * @param endOffset The offset (exclusive) in the data to end the search. + * @param type The type of the NAL unit to search for, or -1 for any NAL unit. + * @param prefixFlags A boolean array whose first three elements are used to store the state + * required to detect NAL units where the NAL unit prefix spans array boundaries. The array + * must be at least 3 elements long. + * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. + */ + public static int findNalUnit(byte[] data, int startOffset, int endOffset, int type, + boolean[] prefixFlags) { + int length = endOffset - startOffset; + + Assertions.checkState(length >= 0); + if (length == 0) { + return endOffset; + } + + if (prefixFlags != null) { + if (prefixFlags[0] && matchesType(data, startOffset, type)) { + clearPrefixFlags(prefixFlags); + return startOffset - 3; + } else if (length > 1 && prefixFlags[1] && data[startOffset] == 1 + && matchesType(data, startOffset + 1, type)) { + clearPrefixFlags(prefixFlags); + return startOffset - 2; + } else if (length > 2 && prefixFlags[2] && data[startOffset] == 0 + && data[startOffset + 1] == 1 && matchesType(data, startOffset + 2, type)) { + clearPrefixFlags(prefixFlags); + return startOffset - 1; + } + } + + int limit = endOffset - 2; + // We're looking for the NAL unit start code prefix 0x000001, followed by a byte that matches + // the specified type. The value of i tracks the index of the third byte in the four bytes + // 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; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java index ab6aff54c6..1961cc7a76 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaption.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer.text.eia608; /** * A Closed Caption that contains textual data associated with time indices. */ -/* package */ abstract class ClosedCaption implements Comparable { +/* package */ abstract class ClosedCaption { /** * Identifies closed captions with control characters. @@ -33,23 +33,9 @@ package com.google.android.exoplayer.text.eia608; * The type of the closed caption data. */ public final int type; - /** - * Timestamp associated with the closed caption. - */ - public final long timeUs; - protected ClosedCaption(int type, long timeUs) { + protected ClosedCaption(int type) { this.type = type; - this.timeUs = timeUs; - } - - @Override - public int compareTo(ClosedCaption another) { - long delta = this.timeUs - another.timeUs; - if (delta == 0) { - return 0; - } - return delta > 0 ? 1 : -1; } } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionCtrl.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionCtrl.java index ceca05c919..c784f50cd9 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionCtrl.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionCtrl.java @@ -70,8 +70,8 @@ package com.google.android.exoplayer.text.eia608; public final byte cc1; public final byte cc2; - protected ClosedCaptionCtrl(byte cc1, byte cc2, long timeUs) { - super(ClosedCaption.TYPE_CTRL, timeUs); + protected ClosedCaptionCtrl(byte cc1, byte cc2) { + super(ClosedCaption.TYPE_CTRL); this.cc1 = cc1; this.cc2 = cc2; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionList.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionList.java new file mode 100644 index 0000000000..f47ec1f466 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionList.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.text.eia608; + +/* package */ final class ClosedCaptionList implements Comparable { + + public final long timeUs; + public final boolean decodeOnly; + public final ClosedCaption[] captions; + + public ClosedCaptionList(long timeUs, boolean decodeOnly, ClosedCaption[] captions) { + this.timeUs = timeUs; + this.decodeOnly = decodeOnly; + this.captions = captions; + } + + @Override + public int compareTo(ClosedCaptionList other) { + long delta = timeUs - other.timeUs; + if (delta == 0) { + return 0; + } + return delta > 0 ? 1 : -1; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionText.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionText.java index 49fbc5af2d..98e93ea493 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionText.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/ClosedCaptionText.java @@ -19,8 +19,8 @@ package com.google.android.exoplayer.text.eia608; public final String text; - public ClosedCaptionText(String text, long timeUs) { - super(ClosedCaption.TYPE_TEXT, timeUs); + public ClosedCaptionText(String text) { + super(ClosedCaption.TYPE_TEXT); this.text = text; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java index 8d4b0f5b06..a855e34839 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java @@ -15,10 +15,12 @@ */ 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.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") @@ -80,28 +82,31 @@ public class Eia608Parser { 0xFB // 3F: 251 'รป' "Latin small letter U with circumflex" }; - private final BitArray seiBuffer; + private final ParsableBitArray seiBuffer; private final StringBuilder stringBuilder; + private final ArrayList captions; /* package */ Eia608Parser() { - seiBuffer = new BitArray(); + seiBuffer = new ParsableBitArray(); stringBuilder = new StringBuilder(); + captions = new ArrayList(); } /* package */ boolean canParse(String mimeType) { return mimeType.equals(MimeTypes.APPLICATION_EIA608); } - /* package */ void parse(byte[] data, int size, long timeUs, List out) { - if (size <= 0) { - return; + /* package */ ClosedCaptionList parse(SampleHolder sampleHolder) { + if (sampleHolder.size <= 0) { + return null; } + captions.clear(); stringBuilder.setLength(0); - seiBuffer.reset(data, size); + seiBuffer.reset(sampleHolder.data.array()); seiBuffer.skipBits(3); // reserved + process_cc_data_flag + zero_bit int ccCount = seiBuffer.readBits(5); - seiBuffer.skipBytes(1); + seiBuffer.skipBits(8); for (int i = 0; i < ccCount; i++) { seiBuffer.skipBits(5); // one_bit + reserved @@ -134,10 +139,10 @@ public class Eia608Parser { // Control character. if (ccData1 < 0x20) { if (stringBuilder.length() > 0) { - out.add(new ClosedCaptionText(stringBuilder.toString(), timeUs)); + captions.add(new ClosedCaptionText(stringBuilder.toString())); stringBuilder.setLength(0); } - out.add(new ClosedCaptionCtrl(ccData1, ccData2, timeUs)); + captions.add(new ClosedCaptionCtrl(ccData1, ccData2)); continue; } @@ -149,8 +154,16 @@ public class Eia608Parser { } if (stringBuilder.length() > 0) { - out.add(new ClosedCaptionText(stringBuilder.toString(), timeUs)); + captions.add(new ClosedCaptionText(stringBuilder.toString())); } + + if (captions.isEmpty()) { + return null; + } + + ClosedCaption[] captionArray = new ClosedCaption[captions.size()]; + captions.toArray(captionArray); + return new ClosedCaptionList(sampleHolder.timeUs, sampleHolder.decodeOnly, captionArray); } private static char getChar(byte ccData) { @@ -170,7 +183,7 @@ public class Eia608Parser { * @param seiBuffer The buffer to read from. * @return The size of closed captions data. */ - public static int parseHeader(BitArray seiBuffer) { + public static int parseHeader(ParsableByteArray seiBuffer) { int b = 0; int payloadType = 0; @@ -197,11 +210,11 @@ public class Eia608Parser { if (countryCode != COUNTRY_CODE) { return 0; } - int providerCode = seiBuffer.readBits(16); + int providerCode = seiBuffer.readUnsignedShort(); if (providerCode != PROVIDER_CODE) { return 0; } - int userIdentifier = seiBuffer.readBits(32); + int userIdentifier = seiBuffer.readInt(); if (userIdentifier != USER_ID) { return 0; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java index 349c1450b8..8e855bf730 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java @@ -31,8 +31,7 @@ import android.os.Looper; import android.os.Message; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.TreeSet; /** * A {@link TrackRenderer} for EIA-608 closed captions in a media stream. @@ -48,6 +47,8 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { // The default number of rows to display in roll-up captions mode. private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4; + // The maximum duration that captions are parsed ahead of the current position. + private static final int MAX_SAMPLE_READAHEAD_US = 5000000; private final SampleSource source; private final Eia608Parser eia608Parser; @@ -56,7 +57,7 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { private final MediaFormatHolder formatHolder; private final SampleHolder sampleHolder; private final StringBuilder captionStringBuilder; - private final List captionBuffer; + private final TreeSet pendingCaptionLists; private int trackIndex; private long currentPositionUs; @@ -85,7 +86,7 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { formatHolder = new MediaFormatHolder(); sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); captionStringBuilder = new StringBuilder(); - captionBuffer = new ArrayList(); + pendingCaptionLists = new TreeSet(); } @Override @@ -122,6 +123,7 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { private void seekToInternal(long positionUs) { currentPositionUs = positionUs; inputStreamEnded = false; + pendingCaptionLists.clear(); clearPendingSample(); captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; setCaptionMode(CC_MODE_UNKNOWN); @@ -138,10 +140,17 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { throw new ExoPlaybackException(e); } - if (!inputStreamEnded && !isSamplePending()) { + if (isSamplePending()) { + maybeParsePendingSample(); + } + + int result = inputStreamEnded ? SampleSource.END_OF_STREAM : SampleSource.SAMPLE_READ; + while (!isSamplePending() && result == SampleSource.SAMPLE_READ) { try { - int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); - if (result == SampleSource.END_OF_STREAM) { + result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); + if (result == SampleSource.SAMPLE_READ) { + maybeParsePendingSample(); + } else if (result == SampleSource.END_OF_STREAM) { inputStreamEnded = true; } } catch (IOException e) { @@ -149,17 +158,18 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { } } - if (isSamplePending() && sampleHolder.timeUs <= currentPositionUs) { - // Parse the pending sample. - eia608Parser.parse(sampleHolder.data.array(), sampleHolder.size, sampleHolder.timeUs, - captionBuffer); - // Consume parsed captions. - consumeCaptionBuffer(); - // Update the renderer, unless the sample was marked for decoding only. - if (!sampleHolder.decodeOnly) { + while (!pendingCaptionLists.isEmpty()) { + if (pendingCaptionLists.first().timeUs > currentPositionUs) { + // We're too early to render any of the pending caption lists. + return; + } + // Remove and consume the next caption list. + ClosedCaptionList nextCaptionList = pendingCaptionLists.pollFirst(); + consumeCaptionList(nextCaptionList); + // Update the renderer, unless the caption list was marked for decoding only. + if (!nextCaptionList.decodeOnly) { invokeRenderer(caption); } - clearPendingSample(); } } @@ -221,14 +231,26 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { textRenderer.onText(text); } - private void consumeCaptionBuffer() { - int captionBufferSize = captionBuffer.size(); + private void maybeParsePendingSample() { + if (sampleHolder.timeUs > currentPositionUs + MAX_SAMPLE_READAHEAD_US) { + // We're too early to parse the sample. + return; + } + ClosedCaptionList holder = eia608Parser.parse(sampleHolder); + clearPendingSample(); + if (holder != null) { + pendingCaptionLists.add(holder); + } + } + + private void consumeCaptionList(ClosedCaptionList captionList) { + int captionBufferSize = captionList.captions.length; if (captionBufferSize == 0) { return; } for (int i = 0; i < captionBufferSize; i++) { - ClosedCaption caption = captionBuffer.get(i); + ClosedCaption caption = captionList.captions[i]; if (caption.type == ClosedCaption.TYPE_CTRL) { ClosedCaptionCtrl captionCtrl = (ClosedCaptionCtrl) caption; if (captionCtrl.isMiscCode()) { @@ -240,7 +262,6 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { handleText((ClosedCaptionText) caption); } } - captionBuffer.clear(); if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { caption = getDisplayCaption(); diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java b/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java index a7d847d5a1..af2ce03a20 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java @@ -96,12 +96,38 @@ public final class BufferPool implements Allocator { allocatedBufferCount += requiredBufferCount - firstNewBufferIndex; for (int i = firstNewBufferIndex; i < requiredBufferCount; i++) { // Use a recycled buffer if one is available. Else instantiate a new one. - buffers[i] = recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount] : - new byte[bufferLength]; + buffers[i] = nextBuffer(); } return buffers; } + /** + * Obtain a single buffer directly from the pool. + *

+ * When the caller has finished with the buffer, it should be returned to the pool by calling + * {@link #releaseDirect(byte[])}. + * + * @return The allocated buffer. + */ + public synchronized byte[] allocateDirect() { + allocatedBufferCount++; + return nextBuffer(); + } + + /** + * Return a single buffer to the pool. + * + * @param buffer The buffer being returned. + */ + public synchronized void releaseDirect(byte[] buffer) { + // Weak sanity check that the buffer probably originated from this pool. + Assertions.checkArgument(buffer.length == bufferLength); + allocatedBufferCount--; + + ensureRecycledBufferCapacity(recycledBufferCount + 1); + recycledBuffers[recycledBufferCount++] = buffer; + } + /** * Returns the buffers belonging to an allocation to the pool. * @@ -112,14 +138,7 @@ public final class BufferPool implements Allocator { allocatedBufferCount -= buffers.length; int newRecycledBufferCount = recycledBufferCount + buffers.length; - if (recycledBuffers.length < newRecycledBufferCount) { - // Expand the capacity of the recycled buffers array. - byte[][] newRecycledBuffers = new byte[newRecycledBufferCount * 2][]; - if (recycledBufferCount > 0) { - System.arraycopy(recycledBuffers, 0, newRecycledBuffers, 0, recycledBufferCount); - } - recycledBuffers = newRecycledBuffers; - } + ensureRecycledBufferCapacity(newRecycledBufferCount); System.arraycopy(buffers, 0, recycledBuffers, recycledBufferCount, buffers.length); recycledBufferCount = newRecycledBufferCount; } @@ -128,6 +147,22 @@ public final class BufferPool implements Allocator { return (int) ((size + bufferLength - 1) / bufferLength); } + private byte[] nextBuffer() { + return recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount] + : new byte[bufferLength]; + } + + private void ensureRecycledBufferCapacity(int requiredCapacity) { + if (recycledBuffers.length < requiredCapacity) { + // Expand the capacity of the recycled buffers array. + byte[][] newRecycledBuffers = new byte[requiredCapacity * 2][]; + if (recycledBufferCount > 0) { + System.arraycopy(recycledBuffers, 0, newRecycledBuffers, 0, recycledBufferCount); + } + recycledBuffers = newRecycledBuffers; + } + } + private class AllocationImpl implements Allocation { private byte[][] buffers; diff --git a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java deleted file mode 100644 index 45d7ec35d0..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/util/BitArray.java +++ /dev/null @@ -1,361 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer.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. - *

- * 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; - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java b/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java new file mode 100644 index 0000000000..a1f224a1f0 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java @@ -0,0 +1,199 @@ +/* + * 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); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java index 9ea78dad13..72b2552722 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ParsableByteArray.java @@ -23,18 +23,69 @@ import java.nio.ByteBuffer; */ public final class ParsableByteArray { - public final byte[] data; + public byte[] data; 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) { this.data = new byte[length]; + limit = data.length; } - /** Returns the number of bytes in the array. */ - public int length() { - return 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 limit. */ + public int limit() { + 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. */ @@ -42,6 +93,11 @@ public final class ParsableByteArray { return position; } + /** Returns the capacity of the array, which may be larger than the limit. */ + public int capacity() { + return data == null ? 0 : data.length; + } + /** * Sets the reading offset in the array. * @@ -51,7 +107,7 @@ public final class ParsableByteArray { */ public void setPosition(int position) { // It is fine for position to be at the end of the array. - Assertions.checkArgument(position >= 0 && position <= data.length); + Assertions.checkArgument(position >= 0 && position <= limit); this.position = position; } @@ -61,10 +117,26 @@ public final class ParsableByteArray { * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the * array. */ + // TODO: Rename to skipBytes so that it's clearer how much data is being skipped in code where + // both ParsableBitArray and ParsableByteArray are in use. public void skip(int bytes) { setPosition(position + bytes); } + /** + * Reads the next {@code length} bytes into {@code bitArray}, and resets the position of + * {@code bitArray} to zero. + * + * @param bitArray The {@link ParsableBitArray} into which the bytes should be read. + * @param length The number of bytes to write. + */ + // TODO: It's possible to have bitArray directly index into the same array as is being wrapped + // by this instance. Decide whether it's worth doing this. + public void readBytes(ParsableBitArray bitArray, int length) { + readBytes(bitArray.getData(), 0, length); + bitArray.setPosition(0); + } + /** * Reads the next {@code length} bytes into {@code buffer} at {@code offset}. * @@ -127,6 +199,22 @@ public final class ParsableByteArray { return result; } + /** + * Reads a Synchsafe integer. + *

+ * Synchsafe integers keep the highest bit of every byte zeroed. A 32 bit synchsafe integer can + * store 28 bits of information. + * + * @return The parsed value. + */ + public int readSynchSafeInt() { + int b1 = readUnsignedByte(); + int b2 = readUnsignedByte(); + int b3 = readUnsignedByte(); + int b4 = readUnsignedByte(); + return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4; + } + /** * Reads the next four bytes as an unsigned integer into an integer, if the top bit is a zero. *