mirror of
https://github.com/samsonjs/media.git
synced 2026-04-10 12:05:47 +00:00
commit
8f0d576fed
38 changed files with 2567 additions and 1847 deletions
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue