Merge pull request #293 from google/dev

dev -> dev-webm-vp9-opus
This commit is contained in:
ojw28 2015-02-13 20:38:09 +00:00
commit 8f0d576fed
38 changed files with 2567 additions and 1847 deletions

View file

@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.demo"
android:versionCode="1100"
android:versionName="1.1.00"
android:versionCode="1200"
android:versionName="1.2.00"
android:theme="@style/RootTheme">
<uses-permission android:name="android.permission.INTERNET"/>

View file

@ -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();
}
}

View file

@ -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) {

View file

@ -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() {}
}

View file

@ -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}

View file

@ -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

View file

@ -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.

View file

@ -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. */

View file

@ -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) {

View file

@ -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;

View file

@ -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 <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
*/
@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());

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAudioSpecificConfig(
audioSpecificConfig);
MediaFormat mediaFormat = MediaFormat.createAudioFormat(MimeTypes.AUDIO_AAC,
MediaFormat.NO_VALUE, audioParams.second, audioParams.first,
Collections.singletonList(audioSpecificConfig));
frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate;
setMediaFormat(mediaFormat);
} else {
adtsScratch.skipBits(10);
}
adtsScratch.skipBits(4);
sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE;
if (hasCrc) {
sampleSize -= CRC_SIZE;
}
}
}

View file

@ -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<byte[]> initializationData = new ArrayList<byte[]>();
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].
* <p>
* 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<Integer> escapePositions = new ArrayList<Integer>();
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;
}
}
}

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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<byte[]> 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<byte[]>();
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.
* <p>
* 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.
* <p>
* 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;
}
}
}
}
}

View file

@ -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.
* <p>
* The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples
* queued prior to the first keyframe are discarded.
*
* @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise.
*/
public boolean getSample(SampleHolder holder) {
boolean foundEligibleSample = advanceToEligibleSample();
if (!foundEligibleSample) {
return false;
}
// Write the sample into the holder.
rollingBuffer.readSample(holder);
needKeyframe = false;
lastReadTimeUs = holder.timeUs;
return true;
}
/**
* Discards samples from the queue up to the specified time.
*
* @param timeUs The time up to which samples should be discarded, in microseconds.
*/
public void discardUntil(long timeUs) {
while (rollingBuffer.peekSample(sampleInfoHolder) && sampleInfoHolder.timeUs < timeUs) {
rollingBuffer.skipSample();
// We're discarding one or more samples. A subsequent read will need to start at a keyframe.
needKeyframe = true;
}
lastReadTimeUs = Long.MIN_VALUE;
}
/**
* Attempts to configure a splice from this queue to the next.
*
* @param nextQueue The queue being spliced to.
* @return Whether the splice was configured successfully.
*/
public boolean configureSpliceTo(SampleQueue nextQueue) {
if (spliceOutTimeUs != Long.MIN_VALUE) {
// We've already configured the splice.
return true;
}
long firstPossibleSpliceTime;
if (rollingBuffer.peekSample(sampleInfoHolder)) {
firstPossibleSpliceTime = sampleInfoHolder.timeUs;
} else {
firstPossibleSpliceTime = lastReadTimeUs + 1;
}
RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer;
while (nextRollingBuffer.peekSample(sampleInfoHolder)
&& (sampleInfoHolder.timeUs < firstPossibleSpliceTime
|| (sampleInfoHolder.flags & C.SAMPLE_FLAG_SYNC) == 0)) {
// Discard samples from the next queue for as long as they are before the earliest possible
// splice time, or not keyframes.
nextRollingBuffer.skipSample();
}
if (nextRollingBuffer.peekSample(sampleInfoHolder)) {
// We've found a keyframe in the next queue that can serve as the splice point. Set the
// splice point now.
spliceOutTimeUs = sampleInfoHolder.timeUs;
return true;
}
return false;
}
/**
* Advances the underlying buffer to the next sample that is eligible to be returned.
*
* @boolean True if an eligible sample was found. False otherwise, in which case the underlying
* buffer has been emptied.
*/
private boolean advanceToEligibleSample() {
boolean haveNext = rollingBuffer.peekSample(sampleInfoHolder);
if (needKeyframe) {
while (haveNext && (sampleInfoHolder.flags & C.SAMPLE_FLAG_SYNC) == 0) {
rollingBuffer.skipSample();
haveNext = rollingBuffer.peekSample(sampleInfoHolder);
}
}
if (!haveNext) {
return false;
}
if (spliceOutTimeUs != Long.MIN_VALUE && sampleInfoHolder.timeUs >= spliceOutTimeUs) {
return false;
}
return true;
}
// Called by the loading thread.
protected boolean writingSample() {
return writingSample;
}
protected void setMediaFormat(MediaFormat mediaFormat) {
this.mediaFormat = mediaFormat;
}
protected void startSample(long sampleTimeUs) {
startSample(sampleTimeUs, 0);
}
protected void startSample(long sampleTimeUs, int offset) {
writingSample = true;
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs);
rollingBuffer.startSample(sampleTimeUs, offset);
}
protected void appendData(ParsableByteArray buffer, int length) {
rollingBuffer.appendData(buffer, length);
}
protected void commitSample(boolean isKeyframe) {
commitSample(isKeyframe, 0);
}
protected void commitSample(boolean isKeyframe, int offset) {
rollingBuffer.commitSample(isKeyframe, offset);
writingSample = false;
}
}

View file

@ -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);
}
}
}

View file

@ -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<SampleQueue> sampleQueues; // Indexed by streamType
private final SparseArray<TsPayloadReader> 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<SampleQueue>();
tsPayloadReaders = new SparseArray<TsPayloadReader>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
lastPts = Long.MIN_VALUE;
}
/**
* Gets the number of available tracks.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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);
}
}
}
}

View file

@ -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<Map<String, Object>> {
@Override
public Map<String, Object> parse(byte[] data, int size)
throws UnsupportedEncodingException, ParserException {
BitArray id3Buffer = new BitArray(data, size);
int id3Size = parseId3Header(id3Buffer);
Map<String, Object> metadata = new HashMap<String, Object>();
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<Map<String, Object>> {
} 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<Map<String, Object>> {
}
/**
* 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<Map<String, Object>> {
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<Map<String, Object>> {
if ((flags & 0x2) != 0) {
int extendedHeaderSize = id3Buffer.readSynchSafeInt();
if (extendedHeaderSize > 4) {
id3Buffer.skipBytes(extendedHeaderSize - 4);
id3Buffer.skip(extendedHeaderSize - 4);
}
id3Size -= extendedHeaderSize;
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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}.
* <p>
* For a NAL unit to be found, its first four bytes must be contained within the part of the
* array being searched.
*
* @param data The data to search.
* @param startOffset The offset (inclusive) in the data to start the search.
* @param endOffset The offset (exclusive) in the data to end the search.
* @param type The type of the NAL unit to search for, or -1 for any NAL unit.
* @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.
*/
public static int findNalUnit(byte[] data, int startOffset, int endOffset, int type) {
return findNalUnit(data, startOffset, endOffset, type, null);
}
/**
* Like {@link #findNalUnit(byte[], int, int, int)}, but supports finding of NAL units across
* array boundaries.
* <p>
* To use this method, pass the same {@code prefixFlags} parameter to successive calls where the
* data passed represents a contiguous stream. The state maintained in this parameter allows the
* detection of NAL units where the NAL unit prefix spans array boundaries.
* <p>
* Note that when using {@code prefixFlags} the return value may be 3, 2 or 1 less than
* {@code startOffset}, to indicate a NAL unit starting 3, 2 or 1 bytes before the first byte in
* the current array.
*
* @param data The data to search.
* @param startOffset The offset (inclusive) in the data to start the search.
* @param endOffset The offset (exclusive) in the data to end the search.
* @param type The type of the NAL unit to search for, or -1 for any NAL unit.
* @param prefixFlags A boolean array whose first three elements are used to store the state
* required to detect NAL units where the NAL unit prefix spans array boundaries. The array
* must be at least 3 elements long.
* @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.
*/
public static int findNalUnit(byte[] data, int startOffset, int endOffset, int type,
boolean[] prefixFlags) {
int length = endOffset - startOffset;
Assertions.checkState(length >= 0);
if (length == 0) {
return endOffset;
}
if (prefixFlags != null) {
if (prefixFlags[0] && matchesType(data, startOffset, type)) {
clearPrefixFlags(prefixFlags);
return startOffset - 3;
} else if (length > 1 && prefixFlags[1] && data[startOffset] == 1
&& matchesType(data, startOffset + 1, type)) {
clearPrefixFlags(prefixFlags);
return startOffset - 2;
} else if (length > 2 && prefixFlags[2] && data[startOffset] == 0
&& data[startOffset + 1] == 1 && matchesType(data, startOffset + 2, type)) {
clearPrefixFlags(prefixFlags);
return startOffset - 1;
}
}
int limit = endOffset - 2;
// We're looking for the NAL unit start code prefix 0x000001, followed by a byte that matches
// the specified type. The value of i tracks the index of the third byte in the four bytes
// being examined.
for (int i = startOffset + 2; i < limit; i += 3) {
if ((data[i] & 0xFE) != 0) {
// There isn't a NAL prefix here, or at the next two positions. Do nothing and let the
// loop advance the index by three.
} else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1
&& matchesType(data, i + 1, type)) {
return i - 2;
} else {
// There isn't a NAL prefix here, but there might be at the next position. We should
// only skip forward by one. The loop will skip forward by three, so subtract two here.
i -= 2;
}
}
if (prefixFlags != null) {
// True if the last three bytes in the data seen so far are {0,0,1}.
prefixFlags[0] = length > 2
? (data[endOffset - 3] == 0 && data[endOffset - 2] == 0 && data[endOffset - 1] == 1)
: length == 2 ? (prefixFlags[2] && data[endOffset - 2] == 0 && data[endOffset - 1] == 1)
: (prefixFlags[1] && data[endOffset - 1] == 1);
// True if the last three bytes in the data seen so far are {0,0}.
prefixFlags[1] = length > 1 ? data[endOffset - 2] == 0 && data[endOffset - 1] == 0
: prefixFlags[2] && data[endOffset - 1] == 0;
// True if the last three bytes in the data seen so far are {0}.
prefixFlags[2] = data[endOffset - 1] == 0;
}
return endOffset;
}
/**
* Like {@link #findNalUnit(byte[], int, int, int)} with {@code type == -1}.
*
* @param data The data to search.
* @param startOffset The offset (inclusive) in the data to start the search.
* @param endOffset The offset (exclusive) in the data to end the search.
* @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.
*/
public static int findNalUnit(byte[] data, int startOffset, int endOffset) {
return findNalUnit(data, startOffset, endOffset, null);
}
/**
* Like {@link #findNalUnit(byte[], int, int, int, boolean[])} with {@code type == -1}.
*
* @param data The data to search.
* @param startOffset The offset (inclusive) in the data to start the search.
* @param endOffset The offset (exclusive) in the data to end the search.
* @param prefixFlags A boolean array of length at least 3.
* @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.
*/
public static int findNalUnit(byte[] data, int startOffset, int endOffset,
boolean[] prefixFlags) {
return findNalUnit(data, startOffset, endOffset, -1, prefixFlags);
}
/**
* Gets the type of the NAL unit in {@code data} that starts at {@code offset}.
*
* @param data The data to search.
* @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and
* {@code data.length - 3} (exclusive).
* @return The type of the unit.
*/
public static int getNalUnitType(byte[] data, int offset) {
return data[offset + 3] & 0x1F;
}
/**
* Clears prefix flags, as used by {@link #findNalUnit(byte[], int, int, int, boolean[])}.
*
* @param prefixFlags The flags to clear.
*/
private static void clearPrefixFlags(boolean[] prefixFlags) {
prefixFlags[0] = false;
prefixFlags[1] = false;
prefixFlags[2] = false;
}
/**
* Returns true if the type at {@code offset} is equal to {@code type}, or if {@code type == -1}.
*/
private static boolean matchesType(byte[] data, int offset, int type) {
return type == -1 || (data[offset] & 0x1F) == type;
}
}

View file

@ -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<ClosedCaption> {
/* 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;
}
}

View file

@ -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;
}

View file

@ -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<ClosedCaptionList> {
public final long timeUs;
public final boolean decodeOnly;
public final ClosedCaption[] captions;
public ClosedCaptionList(long timeUs, boolean decodeOnly, ClosedCaption[] captions) {
this.timeUs = timeUs;
this.decodeOnly = decodeOnly;
this.captions = captions;
}
@Override
public int compareTo(ClosedCaptionList other) {
long delta = timeUs - other.timeUs;
if (delta == 0) {
return 0;
}
return delta > 0 ? 1 : -1;
}
}

View file

@ -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;
}

View file

@ -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<ClosedCaption> captions;
/* package */ Eia608Parser() {
seiBuffer = new BitArray();
seiBuffer = new ParsableBitArray();
stringBuilder = new StringBuilder();
captions = new ArrayList<ClosedCaption>();
}
/* package */ boolean canParse(String mimeType) {
return mimeType.equals(MimeTypes.APPLICATION_EIA608);
}
/* package */ void parse(byte[] data, int size, long timeUs, List<ClosedCaption> 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;
}

View file

@ -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<ClosedCaption> captionBuffer;
private final TreeSet<ClosedCaptionList> 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<ClosedCaption>();
pendingCaptionLists = new TreeSet<ClosedCaptionList>();
}
@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();

View file

@ -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.
* <p>
* When the caller has finished with the buffer, it should be returned to the pool by calling
* {@link #releaseDirect(byte[])}.
*
* @return The allocated buffer.
*/
public synchronized byte[] allocateDirect() {
allocatedBufferCount++;
return nextBuffer();
}
/**
* Return a single buffer to the pool.
*
* @param buffer The buffer being returned.
*/
public synchronized void releaseDirect(byte[] buffer) {
// Weak sanity check that the buffer probably originated from this pool.
Assertions.checkArgument(buffer.length == bufferLength);
allocatedBufferCount--;
ensureRecycledBufferCapacity(recycledBufferCount + 1);
recycledBuffers[recycledBufferCount++] = buffer;
}
/**
* Returns the buffers belonging to an allocation to the pool.
*
@ -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;

View file

@ -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.
* <p>
* The read position must be at a whole byte boundary for this method to be called.
*
* @param out The byte array to copy read data.
* @param offset The offset in the out byte array.
* @param length The length of the data to read
* @throws IllegalStateException If the method is called with the read position not at a whole
* byte boundary.
*/
public void readBytes(byte[] out, int offset, int length) {
Assertions.checkState(bitOffset == 0);
System.arraycopy(data, byteOffset, out, offset, length);
byteOffset += length;
}
/**
* @return The number of whole bytes that are available to read.
*/
public int bytesLeft() {
return limit - byteOffset;
}
/**
* @return Whether or not there is any data available.
*/
public boolean isEmpty() {
return limit == 0;
}
/**
* Reads an unsigned Exp-Golomb-coded format integer.
*
* @return The value of the parsed Exp-Golomb-coded integer.
*/
public int readUnsignedExpGolombCodedInt() {
return readExpGolombCodeNum();
}
/**
* Reads an signed Exp-Golomb-coded format integer.
*
* @return The value of the parsed Exp-Golomb-coded integer.
*/
public int readSignedExpGolombCodedInt() {
int codeNum = readExpGolombCodeNum();
return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2);
}
private int readExpGolombCodeNum() {
int leadingZeros = 0;
while (!readBit()) {
leadingZeros++;
}
return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0);
}
/**
* Reads a Synchsafe integer.
* Synchsafe integers are integers that keep the highest bit of every byte zeroed.
* A 32 bit synchsafe integer can store 28 bits of information.
*
* @return The value of the parsed Synchsafe integer.
*/
public int readSynchSafeInt() {
int b1 = readUnsignedByte();
int b2 = readUnsignedByte();
int b3 = readUnsignedByte();
int b4 = readUnsignedByte();
return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4;
}
// TODO: Find a better place for this method.
/**
* Finds the next Adts sync word.
*
* @return The offset from the current position to the start of the next Adts sync word. If an
* Adts sync word is not found, then the offset to the end of the data is returned.
*/
public int findNextAdtsSyncWord() {
for (int i = byteOffset; i < limit - 1; i++) {
int syncBits = (getUnsignedByte(i) << 8) | getUnsignedByte(i + 1);
if ((syncBits & 0xFFF0) == 0xFFF0 && syncBits != 0xFFFF) {
return i - byteOffset;
}
}
return limit - byteOffset;
}
//TODO: Find a better place for this method.
/**
* Finds the next NAL unit.
*
* @param nalUnitType The type of the NAL unit to search for, or -1 for any NAL unit.
* @param offset The additional offset in the data to start the search from.
* @return The offset from the current position to the start of the NAL unit. If a NAL unit is
* not found, then the offset to the end of the data is returned.
*/
public int findNextNalUnit(int nalUnitType, int offset) {
for (int i = byteOffset + offset; i < limit - 3; i++) {
// Check for NAL unit start code prefix == 0x000001.
if ((data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1)
&& (nalUnitType == -1 || (nalUnitType == (data[i + 3] & 0x1F)))) {
return i - byteOffset;
}
}
return limit - byteOffset;
}
}

View file

@ -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);
}
}

View file

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