diff --git a/README.md b/README.md index 4f70354a6d..7efc281f85 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,16 @@ get started. [Class reference]: http://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/package-summary.html +## Project branches ## + + * The [master][] branch holds the most recent minor release. + * Most development work happens on the [dev][] branch. + * Additional development branches may be established for major features. + +[master]: https://github.com/google/ExoPlayer/tree/master +[dev]: https://github.com/google/ExoPlayer/tree/dev + + ## Using Eclipse ## The repository includes Eclipse projects for both the ExoPlayer library and its diff --git a/demo/build.gradle b/demo/build.gradle index c2916dd0f6..a91c1ab2ca 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -18,7 +18,7 @@ android { buildToolsVersion "19.1" defaultConfig { - minSdkVersion 9 + minSdkVersion 16 targetSdkVersion 19 } buildTypes { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java index 519a252c79..adb28ef0dc 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java @@ -52,7 +52,7 @@ public class SampleChooserActivity extends Activity { sampleAdapter.addAll((Object[]) Samples.SIMPLE); sampleAdapter.add(new Header("YouTube DASH")); sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_MP4); - sampleAdapter.add(new Header("Widevine DASH GTS")); + sampleAdapter.add(new Header("Widevine GTS DASH")); sampleAdapter.addAll((Object[]) Samples.WIDEVINE_GTS); sampleAdapter.add(new Header("SmoothStreaming")); sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java index 7db4240d42..f8306d10d1 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java @@ -91,7 +91,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener } @Override - public void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, + public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) { loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime(); if (VerboseLogUtil.isTagEnabled(TAG)) { @@ -110,13 +110,13 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener } @Override - public void onVideoFormatEnabled(int formatId, int trigger, int mediaTimeMs) { + public void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs) { Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + formatId + ", " + Integer.toString(trigger) + "]"); } @Override - public void onAudioFormatEnabled(int formatId, int trigger, int mediaTimeMs) { + public void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs) { Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + formatId + ", " + Integer.toString(trigger) + "]"); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java index 4d50825205..221d2c6daa 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java @@ -160,8 +160,7 @@ public class DashVodRendererBuilder implements RendererBuilder, } // Build the video renderer. - DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, - bandwidthMeter); + DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); ChunkSource videoChunkSource; String mimeType = videoRepresentations[0].format.mimeType; if (mimeType.equals(MimeTypes.VIDEO_MP4)) { @@ -192,8 +191,7 @@ public class DashVodRendererBuilder implements RendererBuilder, audioChunkSource = null; audioRenderer = null; } else { - DataSource audioDataSource = new HttpDataSource(userAgent, - HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter); + DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); audioTrackNames = new String[audioRepresentationsList.size()]; ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()]; FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index 79934c712b..acf69656ed 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -118,11 +118,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi * A listener for debugging information. */ public interface InfoListener { - void onVideoFormatEnabled(int formatId, int trigger, int mediaTimeMs); - void onAudioFormatEnabled(int formatId, int trigger, int mediaTimeMs); + void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs); + void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs); void onDroppedFrames(int count, long elapsed); void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate); - void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, + void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes); void onLoadCompleted(int sourceId); } @@ -398,7 +398,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } @Override - public void onDownstreamFormatChanged(int sourceId, int formatId, int trigger, int mediaTimeMs) { + public void onDownstreamFormatChanged(int sourceId, String formatId, int trigger, + int mediaTimeMs) { if (infoListener == null) { return; } @@ -469,7 +470,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } @Override - public void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, + public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) { if (infoListener != null) { infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs, diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index 50c7c964bf..5a4e9a58cb 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -150,8 +150,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } // Build the video renderer. - DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, - bandwidthMeter); + DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest, videoStreamElementIndex, videoTrackIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter)); @@ -173,8 +172,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } else { audioTrackNames = new String[audioStreamElementCount]; ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount]; - DataSource audioDataSource = new HttpDataSource(userAgent, - HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter); + DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator(); audioStreamElementCount = 0; for (int i = 0; i < manifest.streamElements.length; i++) { @@ -204,8 +202,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } else { textTrackNames = new String[textStreamElementCount]; ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount]; - DataSource ttmlDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, - bandwidthMeter); + DataSource ttmlDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator(); textStreamElementCount = 0; for (int i = 0; i < manifest.streamElements.length; i++) { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java index ec5bde031f..e3ee3d46b3 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java @@ -115,8 +115,7 @@ import java.util.ArrayList; videoRepresentationsList.toArray(videoRepresentations); // Build the video renderer. - DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, - bandwidthMeter); + DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource, new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, @@ -125,8 +124,7 @@ import java.util.ArrayList; MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); // Build the audio renderer. - DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, - bandwidthMeter); + DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource, new FormatEvaluator.FixedEvaluator(), audioRepresentation); SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java index 80e4c105de..0b92810073 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java @@ -115,8 +115,7 @@ import java.util.ArrayList; } // Build the video renderer. - DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, - bandwidthMeter); + DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest, videoStreamElementIndex, videoTrackIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter)); @@ -126,8 +125,7 @@ import java.util.ArrayList; MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); // Build the audio renderer. - DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, - bandwidthMeter); + DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest, audioStreamElementIndex, new int[] {0}, audioDataSource, new FormatEvaluator.FixedEvaluator()); diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java index 8406b4e17d..4b50a56517 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java @@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - public static final String VERSION = "1.0.10"; + public static final String VERSION = "1.0.11"; /** * The version of the library, expressed as an integer. diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java index a8f34c5039..2ffe4a53f6 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -95,6 +95,10 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; + private static final int START_NOT_SET = 0; + private static final int START_IN_SYNC = 1; + private static final int START_NEED_SYNC = 2; + private final EventListener eventListener; private final ConditionVariable audioTrackReleasingConditionVariable; private final AudioTimestampCompat audioTimestampCompat; @@ -119,7 +123,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { private Method audioTrackGetLatencyMethod; private int audioSessionId; private long submittedBytes; - private boolean audioTrackStartMediaTimeSet; + private int audioTrackStartMediaTimeState; private long audioTrackStartMediaTimeUs; private long audioTrackResumeSystemTimeUs; private long lastReportedCurrentPositionUs; @@ -363,7 +367,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { lastRawPlaybackHeadPosition = 0; rawPlaybackHeadWrapCount = 0; audioTrackStartMediaTimeUs = 0; - audioTrackStartMediaTimeSet = false; + audioTrackStartMediaTimeState = START_NOT_SET; resetSyncParams(); int playState = audioTrack.getPlayState(); if (playState == AudioTrack.PLAYSTATE_PLAYING) { @@ -429,7 +433,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { protected long getCurrentPositionUs() { long systemClockUs = System.nanoTime() / 1000; long currentPositionUs; - if (audioTrack == null || !audioTrackStartMediaTimeSet) { + if (audioTrack == null || audioTrackStartMediaTimeState == START_NOT_SET) { // The AudioTrack hasn't started. currentPositionUs = super.getCurrentPositionUs(); } else if (audioTimestampSet) { @@ -461,7 +465,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } private void maybeSampleSyncParams() { - if (audioTrack == null || !audioTrackStartMediaTimeSet || getState() != STATE_STARTED) { + if (audioTrack == null || audioTrackStartMediaTimeState == START_NOT_SET + || getState() != STATE_STARTED) { // The AudioTrack isn't playing. return; } @@ -549,25 +554,40 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, - MediaCodec.BufferInfo bufferInfo, int bufferIndex) throws ExoPlaybackException { + MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) + throws ExoPlaybackException { + if (shouldSkip) { + codec.releaseOutputBuffer(bufferIndex, false); + codecCounters.skippedOutputBufferCount++; + if (audioTrackStartMediaTimeState == START_IN_SYNC) { + // Skipping the sample will push track time out of sync. We'll need to sync again. + audioTrackStartMediaTimeState = START_NEED_SYNC; + } + return true; + } + if (temporaryBufferSize == 0) { // This is the first time we've seen this {@code buffer}. - // Note: presentationTimeUs corresponds to the end of the sample, not the start. long bufferStartTime = bufferInfo.presentationTimeUs - framesToDurationUs(bufferInfo.size / frameSize); - if (!audioTrackStartMediaTimeSet) { + if (audioTrackStartMediaTimeState == START_NOT_SET) { audioTrackStartMediaTimeUs = Math.max(0, bufferStartTime); - audioTrackStartMediaTimeSet = true; + audioTrackStartMediaTimeState = START_IN_SYNC; } else { // Sanity check that bufferStartTime is consistent with the expected value. long expectedBufferStartTime = audioTrackStartMediaTimeUs + framesToDurationUs(submittedBytes / frameSize); - if (Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) { + if (audioTrackStartMediaTimeState == START_IN_SYNC + && Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) { Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " + bufferStartTime + "]"); - // Adjust audioTrackStartMediaTimeUs to compensate for the discontinuity. Also reset - // lastReportedCurrentPositionUs to allow time to jump backwards if it really wants to. + audioTrackStartMediaTimeState = START_NEED_SYNC; + } + if (audioTrackStartMediaTimeState == START_NEED_SYNC) { + // Adjust audioTrackStartMediaTimeUs to be consistent with the current buffer's start + // time and the number of bytes submitted. Also reset lastReportedCurrentPositionUs to + // allow time to jump backwards if it really wants to. audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime); lastReportedCurrentPositionUs = 0; } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 5d6fb810aa..99a8bee91e 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -391,7 +391,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) { result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false); if (result == SampleSource.SAMPLE_READ) { - currentPositionUs = sampleHolder.timeUs; + if (!sampleHolder.decodeOnly) { + currentPositionUs = sampleHolder.timeUs; + } codecCounters.discardedSamplesCount++; } else if (result == SampleSource.FORMAT_READ) { onInputFormatChanged(formatHolder); @@ -677,15 +679,15 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { return false; } - if (decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs)) { - codec.releaseOutputBuffer(outputIndex, false); - outputIndex = -1; - return true; - } - + boolean decodeOnly = decodeOnlyPresentationTimestamps.contains( + outputBufferInfo.presentationTimeUs); if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo, - outputIndex)) { - currentPositionUs = outputBufferInfo.presentationTimeUs; + outputIndex, decodeOnly)) { + if (decodeOnly) { + decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs); + } else { + currentPositionUs = outputBufferInfo.presentationTimeUs; + } outputIndex = -1; return true; } @@ -701,7 +703,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { * @throws ExoPlaybackException If an error occurs processing the output buffer. */ protected abstract boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, - MediaCodec.BufferInfo bufferInfo, int bufferIndex) throws ExoPlaybackException; + MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) + throws ExoPlaybackException; /** * Returns the name of the secure variant of a given decoder. diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java index f99c328482..98ec611d2a 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -29,7 +29,7 @@ import android.view.Surface; import java.nio.ByteBuffer; /** - * Decodes and renders video using {@MediaCodec}. + * Decodes and renders video using {@link MediaCodec}. */ @TargetApi(16) public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { @@ -338,7 +338,12 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { @Override protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, - MediaCodec.BufferInfo bufferInfo, int bufferIndex) { + MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) { + if (shouldSkip) { + skipOutputBuffer(codec, bufferIndex); + return true; + } + long earlyUs = bufferInfo.presentationTimeUs - timeUs; if (earlyUs < -30000) { // We're more than 30ms late rendering the frame. @@ -371,6 +376,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { return false; } + private void skipOutputBuffer(MediaCodec codec, int bufferIndex) { + TraceUtil.beginSection("skipVideoBuffer"); + codec.releaseOutputBuffer(bufferIndex, false); + TraceUtil.endSection(); + codecCounters.skippedOutputBufferCount++; + } + private void dropOutputBuffer(MediaCodec codec, int bufferIndex) { TraceUtil.beginSection("dropVideoBuffer"); codec.releaseOutputBuffer(bufferIndex, false); diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java index 8e2afc32d6..9c5d6aa303 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -54,8 +54,8 @@ public interface SampleSource { * Prepares the source. *
* Preparation may require reading from the data source (e.g. to determine the available tracks
- * and formats). If insufficient data is available then the call will return rather than block.
- * The method can be called repeatedly until the return value indicates success.
+ * and formats). If insufficient data is available then the call will return {@code false} rather
+ * than block. The method can be called repeatedly until the return value indicates success.
*
* @return True if the source was prepared successfully, false otherwise.
* @throws IOException If an error occurred preparing the source.
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
index 5800acca26..61ad0c93ee 100644
--- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
@@ -59,7 +59,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
* load is for initialization data.
* @param totalBytes The length of the data being loaded in bytes.
*/
- void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization,
+ void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
/**
@@ -126,7 +126,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
* {@link ChunkSource}.
* @param mediaTimeMs The media time at which the change occurred.
*/
- void onDownstreamFormatChanged(int sourceId, int formatId, int trigger, int mediaTimeMs);
+ void onDownstreamFormatChanged(int sourceId, String formatId, int trigger, int mediaTimeMs);
}
@@ -160,6 +160,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
+ private MediaFormat downstreamMediaFormat;
private volatile Format downstreamFormat;
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
@@ -221,6 +222,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
chunkSource.enable();
loadControl.register(this, bufferSizeContribution);
downstreamFormat = null;
+ downstreamMediaFormat = null;
downstreamPositionUs = timeUs;
lastSeekPositionUs = timeUs;
restartFrom(timeUs);
@@ -288,21 +290,30 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
return readData(track, playbackPositionUs, formatHolder, sampleHolder, false);
} else if (mediaChunk.isLastChunk()) {
return END_OF_STREAM;
- } else {
- IOException chunkSourceException = chunkSource.getError();
- if (chunkSourceException != null) {
- throw chunkSourceException;
- }
- return NOTHING_READ;
}
- } else if (downstreamFormat == null || downstreamFormat.id != mediaChunk.format.id) {
+ IOException chunkSourceException = chunkSource.getError();
+ if (chunkSourceException != null) {
+ throw chunkSourceException;
+ }
+ return NOTHING_READ;
+ }
+
+ if (downstreamFormat == null || !downstreamFormat.equals(mediaChunk.format)) {
notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger,
mediaChunk.startTimeUs);
- MediaFormat format = mediaChunk.getMediaFormat();
- chunkSource.getMaxVideoDimensions(format);
- formatHolder.format = format;
- formatHolder.drmInitData = mediaChunk.getPsshInfo();
downstreamFormat = mediaChunk.format;
+ }
+
+ if (!mediaChunk.prepare()) {
+ return NOTHING_READ;
+ }
+
+ MediaFormat mediaFormat = mediaChunk.getMediaFormat();
+ if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat)) {
+ chunkSource.getMaxVideoDimensions(mediaFormat);
+ formatHolder.format = mediaFormat;
+ formatHolder.drmInitData = mediaChunk.getPsshInfo();
+ downstreamMediaFormat = mediaFormat;
return FORMAT_READ;
}
@@ -430,6 +441,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
notifyUpstreamError(e);
+ chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e);
updateLoadControl();
}
@@ -653,7 +665,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
return (int) (timeUs / 1000);
}
- private void notifyLoadStarted(final int formatId, final int trigger,
+ private void notifyLoadStarted(final String formatId, final int trigger,
final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs,
final long totalBytes) {
if (eventHandler != null && eventListener != null) {
@@ -724,7 +736,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
}
}
- private void notifyDownstreamFormatChanged(final int formatId, final int trigger,
+ private void notifyDownstreamFormatChanged(final String formatId, final int trigger,
final long mediaTimeUs) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java
index a68d81fab8..67330957ba 100644
--- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java
@@ -58,7 +58,7 @@ public interface ChunkSource {
*
* @param queue A representation of the currently buffered {@link MediaChunk}s.
*/
- void disable(List
+ * Preparation may require consuming some of the chunk. If the data is not yet available then
+ * this method will return {@code false} rather than block. The method can be called repeatedly
+ * until the return value indicates success.
+ *
+ * @return True if the chunk was prepared. False otherwise.
+ * @throws ParserException If an error occurs parsing the media data.
+ */
+ public abstract boolean prepare() throws ParserException;
+
/**
* Reads the next media sample from the chunk.
+ *
+ * Should only be called after the chunk has been successfully prepared.
*
* @param holder A holder to store the read sample.
* @return True if a sample was read. False if more data is still required.
@@ -101,6 +113,8 @@ public abstract class MediaChunk extends Chunk {
/**
* Returns the media format of the samples contained within this chunk.
+ *
+ * Should only be called after the chunk has been successfully prepared.
*
* @return The sample media format.
*/
@@ -108,6 +122,8 @@ public abstract class MediaChunk extends Chunk {
/**
* Returns the pssh information associated with the chunk.
+ *
+ * Should only be called after the chunk has been successfully prepared.
*
* @return The pssh information.
*/
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java
index 0b1e22b643..8aaee879e4 100644
--- a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java
@@ -33,27 +33,43 @@ import java.util.UUID;
public final class Mp4MediaChunk extends MediaChunk {
private final FragmentedMp4Extractor extractor;
+ private final boolean maybeSelfContained;
private final long sampleOffsetUs;
+ private boolean prepared;
+ private MediaFormat mediaFormat;
+ private Map
+ * Two types are consistent if they are equal, or if one is {@link AdaptationSet#TYPE_UNKNOWN}.
+ * Where one of the types is {@link AdaptationSet#TYPE_UNKNOWN}, the other is returned.
+ *
+ * @param firstType The first type.
+ * @param secondType The second type.
+ * @return The consistent type.
+ */
+ private int checkAdaptationSetTypeConsistency(int firstType, int secondType) {
+ if (firstType == AdaptationSet.TYPE_UNKNOWN) {
+ return secondType;
+ } else if (secondType == AdaptationSet.TYPE_UNKNOWN) {
+ return firstType;
+ } else {
+ Assertions.checkState(firstType == secondType);
+ return firstType;
+ }
+ }
+
/**
* Parses a ContentProtection element.
*
@@ -211,99 +227,194 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return new ContentProtection(schemeUriId, null);
}
- private Representation parseRepresentation(XmlPullParser xpp, String contentId, long periodStart,
- long periodDuration, String parentMimeType, List
+ * The uri is built according to the following rules:
+ *
+ * A merge is successful if both instances define the same {@link Uri}, and if one starte the
+ * byte after the other ends, forming a contiguous region with no overlap.
+ *
+ * If {@code other} is null then the merge is considered unsuccessful, and null is returned.
+ *
+ * @param other The {@link RangedUri} to merge.
+ * @return The merged {@link RangedUri} if the merge was successful. Null otherwise.
+ */
+ public RangedUri attemptMerge(RangedUri other) {
+ if (other == null || !getUri().equals(other.getUri())) {
+ return null;
+ } else if (length != -1 && start + length == other.start) {
+ return new RangedUri(baseUri, stringUri, start,
+ other.length == -1 ? -1 : length + other.length);
+ } else if (other.length != -1 && other.start + other.length == start) {
+ return new RangedUri(baseUri, stringUri, other.start,
+ length == -1 ? -1 : other.length + length);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + (int) start;
+ result = 31 * result + (int) length;
+ result = 31 * result + getUri().hashCode();
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ RangedUri other = (RangedUri) obj;
+ return this.start == other.start
+ && this.length == other.length
+ && getUri().equals(other.getUri());
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java
index e5b11e94ca..d089ba7f59 100644
--- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java
@@ -16,13 +16,16 @@
package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.chunk.Format;
+import com.google.android.exoplayer.dash.DashSegmentIndex;
+import com.google.android.exoplayer.dash.mpd.SegmentBase.MultiSegmentBase;
+import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
import android.net.Uri;
/**
- * A flat version of a DASH representation.
+ * A DASH representation.
*/
-public class Representation {
+public abstract class Representation {
/**
* Identifies the piece of content to which this {@link Representation} belongs.
@@ -33,7 +36,7 @@ public class Representation {
public final String contentId;
/**
- * Identifies the revision of the {@link Representation}.
+ * Identifies the revision of the content.
*
* If the media for a given ({@link #contentId} can change over time without a change to the
* {@link #format}'s {@link Format#id} (e.g. as a result of re-encoding the media with an
@@ -43,45 +46,93 @@ public class Representation {
public final long revisionId;
/**
- * The format in which the {@link Representation} is encoded.
+ * The format of the representation.
*/
public final Format format;
- public final long contentLength;
+ /**
+ * The start time of the enclosing period in milliseconds since the epoch.
+ */
+ public final long periodStartMs;
- public final long initializationStart;
+ /**
+ * The duration of the enclosing period in milliseconds.
+ */
+ public final long periodDurationMs;
- public final long initializationEnd;
+ /**
+ * The offset of the presentation timestamps in the media stream relative to media time.
+ */
+ public final long presentationTimeOffsetMs;
- public final long indexStart;
+ private final RangedUri initializationUri;
- public final long indexEnd;
+ /**
+ * Constructs a new instance.
+ *
+ * @param periodStartMs The start time of the enclosing period in milliseconds.
+ * @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
+ * duration is unknown.
+ * @param contentId Identifies the piece of content to which this representation belongs.
+ * @param revisionId Identifies the revision of the content.
+ * @param format The format of the representation.
+ * @param segmentBase A segment base element for the representation.
+ * @return The constructed instance.
+ */
+ public static Representation newInstance(long periodStartMs, long periodDurationMs,
+ String contentId, long revisionId, Format format, SegmentBase segmentBase) {
+ if (segmentBase instanceof SingleSegmentBase) {
+ return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
+ format, (SingleSegmentBase) segmentBase, -1);
+ } else if (segmentBase instanceof MultiSegmentBase) {
+ return new MultiSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
+ format, (MultiSegmentBase) segmentBase);
+ } else {
+ throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or "
+ + "MultiSegmentBase");
+ }
+ }
- public final long periodStart;
-
- public final long periodDuration;
-
- public final Uri uri;
-
- public Representation(String contentId, long revisionId, Format format, Uri uri,
- long contentLength, long initializationStart, long initializationEnd, long indexStart,
- long indexEnd, long periodStart, long periodDuration) {
+ private Representation(long periodStartMs, long periodDurationMs, String contentId,
+ long revisionId, Format format, SegmentBase segmentBase) {
+ this.periodStartMs = periodStartMs;
+ this.periodDurationMs = periodDurationMs;
this.contentId = contentId;
this.revisionId = revisionId;
this.format = format;
- this.contentLength = contentLength;
- this.initializationStart = initializationStart;
- this.initializationEnd = initializationEnd;
- this.indexStart = indexStart;
- this.indexEnd = indexEnd;
- this.periodStart = periodStart;
- this.periodDuration = periodDuration;
- this.uri = uri;
+ initializationUri = segmentBase.getInitialization(this);
+ presentationTimeOffsetMs = (segmentBase.presentationTimeOffset * 1000) / segmentBase.timescale;
}
+ /**
+ * Gets a {@link RangedUri} defining the location of the representation's initialization data.
+ * May be null if no initialization data exists.
+ *
+ * @return A {@link RangedUri} defining the location of the initialization data, or null.
+ */
+ public RangedUri getInitializationUri() {
+ return initializationUri;
+ }
+
+ /**
+ * Gets a {@link RangedUri} defining the location of the representation's segment index. Null if
+ * the representation provides an index directly.
+ *
+ * @return The location of the segment index, or null.
+ */
+ public abstract RangedUri getIndexUri();
+
+ /**
+ * Gets a segment index, if the representation is able to provide one directly. Null if the
+ * segment index is defined externally.
+ *
+ * @return The segment index, or null.
+ */
+ public abstract DashSegmentIndex getIndex();
+
/**
* Generates a cache key for the {@link Representation}, in the format
- * {@link #contentId}.{@link #format.id}.{@link #revisionId}.
+ * {@code contentId + "." + format.id + "." + revisionId}.
*
* @return A cache key.
*/
@@ -89,4 +140,143 @@ public class Representation {
return contentId + "." + format.id + "." + revisionId;
}
+ /**
+ * A DASH representation consisting of a single segment.
+ */
+ public static class SingleSegmentRepresentation extends Representation {
+
+ /**
+ * The {@link Uri} of the single segment.
+ */
+ public final Uri uri;
+
+ /**
+ * The content length, or -1 if unknown.
+ */
+ public final long contentLength;
+
+ private final RangedUri indexUri;
+
+ /**
+ * @param periodStartMs The start time of the enclosing period in milliseconds.
+ * @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
+ * duration is unknown.
+ * @param contentId Identifies the piece of content to which this representation belongs.
+ * @param revisionId Identifies the revision of the content.
+ * @param format The format of the representation.
+ * @param uri The uri of the media.
+ * @param initializationStart The offset of the first byte of initialization data.
+ * @param initializationEnd The offset of the last byte of initialization data.
+ * @param indexStart The offset of the first byte of index data.
+ * @param indexEnd The offset of the last byte of index data.
+ * @param contentLength The content length, or -1 if unknown.
+ */
+ public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs,
+ String contentId, long revisionId, Format format, Uri uri, long initializationStart,
+ long initializationEnd, long indexStart, long indexEnd, long contentLength) {
+ RangedUri rangedUri = new RangedUri(uri, null, initializationStart,
+ initializationEnd - initializationStart + 1);
+ SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, uri, indexStart,
+ indexEnd - indexStart + 1);
+ return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
+ format, segmentBase, contentLength);
+ }
+
+ /**
+ * @param periodStartMs The start time of the enclosing period in milliseconds.
+ * @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
+ * duration is unknown.
+ * @param contentId Identifies the piece of content to which this representation belongs.
+ * @param revisionId Identifies the revision of the content.
+ * @param format The format of the representation.
+ * @param segmentBase The segment base underlying the representation.
+ * @param contentLength The content length, or -1 if unknown.
+ */
+ public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
+ long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) {
+ super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
+ this.uri = segmentBase.uri;
+ this.indexUri = segmentBase.getIndex();
+ this.contentLength = contentLength;
+ }
+
+ @Override
+ public RangedUri getIndexUri() {
+ return indexUri;
+ }
+
+ @Override
+ public DashSegmentIndex getIndex() {
+ return null;
+ }
+
+ }
+
+ /**
+ * A DASH representation consisting of multiple segments.
+ */
+ public static class MultiSegmentRepresentation extends Representation
+ implements DashSegmentIndex {
+
+ private final MultiSegmentBase segmentBase;
+
+ /**
+ * @param periodStartMs The start time of the enclosing period in milliseconds.
+ * @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
+ * duration is unknown.
+ * @param contentId Identifies the piece of content to which this representation belongs.
+ * @param revisionId Identifies the revision of the content.
+ * @param format The format of the representation.
+ * @param segmentBase The segment base underlying the representation.
+ */
+ public MultiSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
+ long revisionId, Format format, MultiSegmentBase segmentBase) {
+ super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
+ this.segmentBase = segmentBase;
+ }
+
+ @Override
+ public RangedUri getIndexUri() {
+ return null;
+ }
+
+ @Override
+ public DashSegmentIndex getIndex() {
+ return this;
+ }
+
+ // DashSegmentIndex implementation.
+
+ @Override
+ public RangedUri getSegmentUrl(int segmentIndex) {
+ return segmentBase.getSegmentUrl(this, segmentIndex);
+ }
+
+ @Override
+ public int getSegmentNum(long timeUs) {
+ return segmentBase.getSegmentNum(timeUs);
+ }
+
+ @Override
+ public long getTimeUs(int segmentIndex) {
+ return segmentBase.getSegmentTimeUs(segmentIndex);
+ }
+
+ @Override
+ public long getDurationUs(int segmentIndex) {
+ return segmentBase.getSegmentDurationUs(segmentIndex);
+ }
+
+ @Override
+ public int getFirstSegmentNum() {
+ return segmentBase.getFirstSegmentNum();
+ }
+
+ @Override
+ public int getLastSegmentNum() {
+ return segmentBase.getLastSegmentNum();
+ }
+
+ }
+
}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Segment.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Segment.java
deleted file mode 100644
index 681c7aae12..0000000000
--- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Segment.java
+++ /dev/null
@@ -1,81 +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.dash.mpd;
-
-/**
- * Represents a particular segment in a Representation.
- *
- */
-public abstract class Segment {
-
- public final String relativeUri;
-
- public final long sequenceNumber;
-
- public final long duration;
-
- public Segment(String relativeUri, long sequenceNumber, long duration) {
- this.relativeUri = relativeUri;
- this.sequenceNumber = sequenceNumber;
- this.duration = duration;
- }
-
- /**
- * Represents a timeline segment from the MPD's SegmentTimeline list.
- */
- public static class Timeline extends Segment {
-
- public Timeline(long sequenceNumber, long duration) {
- super(null, sequenceNumber, duration);
- }
-
- }
-
- /**
- * Represents an initialization segment.
- */
- public static class Initialization extends Segment {
-
- public final long initializationStart;
- public final long initializationEnd;
-
- public Initialization(String relativeUri, long initializationStart,
- long initializationEnd) {
- super(relativeUri, -1, -1);
- this.initializationStart = initializationStart;
- this.initializationEnd = initializationEnd;
- }
-
- }
-
- /**
- * Represents a media segment.
- */
- public static class Media extends Segment {
-
- public final long mediaStart;
-
- public Media(String relativeUri, long sequenceNumber, long duration) {
- this(relativeUri, 0, sequenceNumber, duration);
- }
-
- public Media(String uri, long mediaStart, long sequenceNumber, long duration) {
- super(uri, sequenceNumber, duration);
- this.mediaStart = mediaStart;
- }
-
- }
-}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentedRepresentation.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentedRepresentation.java
deleted file mode 100644
index 53f14c3852..0000000000
--- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentedRepresentation.java
+++ /dev/null
@@ -1,49 +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.dash.mpd;
-
-import com.google.android.exoplayer.chunk.Format;
-import com.google.android.exoplayer.upstream.DataSpec;
-
-import android.net.Uri;
-
-import java.util.List;
-
-/**
- * Represents a DASH Representation which uses the SegmentList structure (i.e. it has a list of
- * Segment URLs instead of a single URL).
- */
-public class SegmentedRepresentation extends Representation {
-
- private List
+ * URLs are built according to the substitution rules defined in ISO/IEC 23009-1:2014 5.3.9.4.4.
+ */
+public final class UrlTemplate {
+
+ private static final String REPRESENTATION = "RepresentationID";
+ private static final String NUMBER = "Number";
+ private static final String BANDWIDTH = "Bandwidth";
+ private static final String TIME = "Time";
+ private static final String ESCAPED_DOLLAR = "$$";
+ private static final String DEFAULT_FORMAT_TAG = "%01d";
+
+ private static final int REPRESENTATION_ID = 1;
+ private static final int NUMBER_ID = 2;
+ private static final int BANDWIDTH_ID = 3;
+ private static final int TIME_ID = 4;
+
+ private final String[] urlPieces;
+ private final int[] identifiers;
+ private final String[] identifierFormatTags;
+ private final int identifierCount;
+
+ /**
+ * Compile an instance from the provided template string.
+ *
+ * @param template The template.
+ * @return The compiled instance.
+ * @throws IllegalArgumentException If the template string is malformed.
+ */
+ public static UrlTemplate compile(String template) {
+ // These arrays are sizes assuming each of the four possible identifiers will be present at
+ // most once in the template, which seems like a reasonable assumption.
+ String[] urlPieces = new String[5];
+ int[] identifiers = new int[4];
+ String[] identifierFormatTags = new String[4];
+ int identifierCount = parseTemplate(template, urlPieces, identifiers, identifierFormatTags);
+ return new UrlTemplate(urlPieces, identifiers, identifierFormatTags, identifierCount);
+ }
+
+ /**
+ * Internal constructor. Use {@link #compile(String)} to build instances of this class.
+ */
+ private UrlTemplate(String[] urlPieces, int[] identifiers, String[] identifierFormatTags,
+ int identifierCount) {
+ this.urlPieces = urlPieces;
+ this.identifiers = identifiers;
+ this.identifierFormatTags = identifierFormatTags;
+ this.identifierCount = identifierCount;
+ }
+
+ /**
+ * Constructs a Uri from the template, substituting in the provided arguments.
+ *
+ * Arguments whose corresponding identifiers are not present in the template will be ignored.
+ *
+ * @param representationId The representation identifier.
+ * @param segmentNumber The segment number.
+ * @param bandwidth The bandwidth.
+ * @param time The time as specified by the segment timeline.
+ * @return The built Uri.
+ */
+ public String buildUri(String representationId, int segmentNumber, int bandwidth, long time) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < identifierCount; i++) {
+ builder.append(urlPieces[i]);
+ if (identifiers[i] == REPRESENTATION_ID) {
+ builder.append(representationId);
+ } else if (identifiers[i] == NUMBER_ID) {
+ builder.append(String.format(identifierFormatTags[i], segmentNumber));
+ } else if (identifiers[i] == BANDWIDTH_ID) {
+ builder.append(String.format(identifierFormatTags[i], bandwidth));
+ } else if (identifiers[i] == TIME_ID) {
+ builder.append(String.format(identifierFormatTags[i], time));
+ }
+ }
+ builder.append(urlPieces[identifierCount]);
+ return builder.toString();
+ }
+
+ /**
+ * Parses {@code template}, placing the decomposed components into the provided arrays.
+ *
+ * If the return value is N, {@code urlPieces} will contain (N+1) strings that must be
+ * interleaved with N arguments in order to construct a url. The N identifiers that correspond to
+ * the required arguments, together with the tags that define their required formatting, are
+ * returned in {@code identifiers} and {@code identifierFormatTags} respectively.
+ *
+ * @param template The template to parse.
+ * @param urlPieces A holder for pieces of url parsed from the template.
+ * @param identifiers A holder for identifiers parsed from the template.
+ * @param identifierFormatTags A holder for format tags corresponding to the parsed identifiers.
+ * @return The number of identifiers in the template url.
+ * @throws IllegalArgumentException If the template string is malformed.
+ */
+ private static int parseTemplate(String template, String[] urlPieces, int[] identifiers,
+ String[] identifierFormatTags) {
+ urlPieces[0] = "";
+ int templateIndex = 0;
+ int identifierCount = 0;
+ while (templateIndex < template.length()) {
+ int dollarIndex = template.indexOf("$", templateIndex);
+ if (dollarIndex == -1) {
+ urlPieces[identifierCount] += template.substring(templateIndex);
+ templateIndex = template.length();
+ } else if (dollarIndex != templateIndex) {
+ urlPieces[identifierCount] += template.substring(templateIndex, dollarIndex);
+ templateIndex = dollarIndex;
+ } else if (template.startsWith(ESCAPED_DOLLAR, templateIndex)) {
+ urlPieces[identifierCount] += "$";
+ templateIndex += 2;
+ } else {
+ int secondIndex = template.indexOf("$", templateIndex + 1);
+ String identifier = template.substring(templateIndex + 1, secondIndex);
+ if (identifier.equals(REPRESENTATION)) {
+ identifiers[identifierCount] = REPRESENTATION_ID;
+ } else {
+ int formatTagIndex = identifier.indexOf("%0");
+ String formatTag = DEFAULT_FORMAT_TAG;
+ if (formatTagIndex != -1) {
+ formatTag = identifier.substring(formatTagIndex);
+ if (!formatTag.endsWith("d")) {
+ formatTag += "d";
+ }
+ identifier = identifier.substring(0, formatTagIndex);
+ }
+ if (identifier.equals(NUMBER)) {
+ identifiers[identifierCount] = NUMBER_ID;
+ } else if (identifier.equals(BANDWIDTH)) {
+ identifiers[identifierCount] = BANDWIDTH_ID;
+ } else if (identifier.equals(TIME)) {
+ identifiers[identifierCount] = TIME_ID;
+ } else {
+ throw new IllegalArgumentException("Invalid template: " + template);
+ }
+ identifierFormatTags[identifierCount] = formatTag;
+ }
+ identifierCount++;
+ urlPieces[identifierCount] = "";
+ templateIndex = secondIndex + 1;
+ }
+ }
+ return identifierCount;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java
index 643dbd8205..716ca458bf 100644
--- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java
+++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java
@@ -21,6 +21,7 @@ import java.util.List;
/* package */ abstract class Atom {
public static final int TYPE_avc1 = 0x61766331;
+ public static final int TYPE_avc3 = 0x61766333;
public static final int TYPE_esds = 0x65736473;
public static final int TYPE_mdat = 0x6D646174;
public static final int TYPE_mfhd = 0x6D666864;
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java
index 2c46b5a93b..16e1788943 100644
--- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java
+++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java
@@ -49,6 +49,15 @@ import java.util.UUID;
*/
public final class FragmentedMp4Extractor {
+ /**
+ * Flag to work around an issue in some video streams where every frame is marked as a sync frame.
+ * The workaround overrides the sync frame flags in the stream, forcing them to false except for
+ * the first sample in each segment.
+ *
+ * This flag does nothing if the stream is not a video stream.
+ */
+ public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
+
/**
* An attempt to read from the input stream returned 0 bytes of data.
*/
@@ -74,9 +83,13 @@ public final class FragmentedMp4Extractor {
* A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}.
*/
public static final int RESULT_READ_SIDX = 32;
+ /**
+ * The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
+ */
+ public static final int RESULT_NEED_SAMPLE_HOLDER = 64;
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
- | RESULT_READ_SAMPLE_FULL;
+ | RESULT_READ_SAMPLE_FULL | RESULT_NEED_SAMPLE_HOLDER;
private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
@@ -97,6 +110,7 @@ public final class FragmentedMp4Extractor {
static {
HashSet EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
+ * It was originally designed for the Matroska container format. More information about EBML and
+ * Matroska is available here.
+ */
+/* package */ final class DefaultEbmlReader implements EbmlReader {
+
+ // State values used in variables state, elementIdState, elementContentSizeState, and
+ // varintBytesState.
+ private static final int STATE_BEGIN_READING = 0;
+ private static final int STATE_READ_CONTENTS = 1;
+ private static final int STATE_FINISHED_READING = 2;
+
+ /**
+ * The first byte of a variable-length integer (varint) will have one of these bit masks
+ * indicating the total length in bytes.
+ *
+ * {@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes.
+ */
+ private static final int[] VARINT_LENGTH_MASKS = new int[] {
+ 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
+ };
+
+ private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8;
+ private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
+ private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
+
+ /**
+ * Scratch space to read in EBML varints, unsigned ints, and floats - each of which can be
+ * up to 8 bytes.
+ */
+ private final byte[] tempByteArray = new byte[8];
+ private final Stack Returns {@link #READ_RESULT_CONTINUE} if an entire element size has been
+ * read into {@link #elementContentSize}. Reset {@link #elementContentSizeState} to
+ * {@link #STATE_BEGIN_READING} before calling to indicate a new element size should be read.
+ *
+ * @param inputStream The input stream from which an element size should be read
+ * @return One of the {@code RESULT_*} flags defined in this class
+ */
+ private int readElementContentSize(NonBlockingInputStream inputStream) {
+ if (elementContentSizeState == STATE_FINISHED_READING) {
+ return READ_RESULT_CONTINUE;
+ }
+ if (elementContentSizeState == STATE_BEGIN_READING) {
+ varintBytesState = STATE_BEGIN_READING;
+ elementContentSizeState = STATE_READ_CONTENTS;
+ }
+ int result = readVarintBytes(inputStream);
+ if (result != READ_RESULT_CONTINUE) {
+ return result;
+ }
+ elementContentSize = getTempByteArrayValue(varintBytesLength, true);
+ elementContentSizeState = STATE_FINISHED_READING;
+ return READ_RESULT_CONTINUE;
+ }
+
+ /**
+ * Reads an EBML variable-length integer (varint) such that reading can be stopped and started
+ * again in a later call if not enough bytes are available.
+ *
+ * Returns {@link #READ_RESULT_CONTINUE} if an entire varint has been read into
+ * {@link #tempByteArray} and the length of the varint is in {@link #varintBytesLength}.
+ * Reset {@link #varintBytesState} to {@link #STATE_BEGIN_READING} before calling to indicate
+ * a new varint should be read.
+ *
+ * @param inputStream The input stream from which a varint should be read
+ * @return One of the {@code RESULT_*} flags defined in this class
+ */
+ private int readVarintBytes(NonBlockingInputStream inputStream) {
+ if (varintBytesState == STATE_FINISHED_READING) {
+ return READ_RESULT_CONTINUE;
+ }
+
+ // Read first byte to get length.
+ if (varintBytesState == STATE_BEGIN_READING) {
+ bytesState = 0;
+ int result = readBytesInternal(inputStream, tempByteArray, 1);
+ if (result != READ_RESULT_CONTINUE) {
+ return result;
+ }
+ varintBytesState = STATE_READ_CONTENTS;
+
+ int firstByte = tempByteArray[0] & 0xff;
+ varintBytesLength = -1;
+ for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
+ if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
+ varintBytesLength = i + 1;
+ break;
+ }
+ }
+ if (varintBytesLength == -1) {
+ throw new IllegalStateException(
+ "No valid varint length mask found at bytesRead = " + bytesRead);
+ }
+ }
+
+ // Read remaining bytes.
+ int result = readBytesInternal(inputStream, tempByteArray, varintBytesLength);
+ if (result != READ_RESULT_CONTINUE) {
+ return result;
+ }
+
+ // All bytes have been read.
+ return READ_RESULT_CONTINUE;
+ }
+
+ /**
+ * Reads a set amount of bytes into a {@link ByteBuffer} such that reading can be stopped
+ * and started again later if not enough bytes are available.
+ *
+ * Returns {@link #READ_RESULT_CONTINUE} if all bytes have been read. Reset
+ * {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
+ *
+ * @param inputStream The input stream from which bytes should be read
+ * @param byteBuffer The {@link ByteBuffer} into which bytes should be read
+ * @param totalBytes The total size of bytes to be read
+ * @return One of the {@code RESULT_*} flags defined in this class
+ */
+ private int readBytesInternal(
+ NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
+ if (bytesState == STATE_BEGIN_READING && totalBytes > byteBuffer.capacity()) {
+ throw new IllegalArgumentException("Byte buffer not large enough");
+ }
+ if (bytesState >= totalBytes) {
+ return READ_RESULT_CONTINUE;
+ }
+ int remainingBytes = totalBytes - bytesState;
+ int additionalBytesRead = inputStream.read(byteBuffer, remainingBytes);
+ return updateBytesState(additionalBytesRead, totalBytes);
+ }
+
+ /**
+ * Reads a set amount of bytes into a {@code byte[]} such that reading can be stopped
+ * and started again later if not enough bytes are available.
+ *
+ * Returns {@link #READ_RESULT_CONTINUE} if all bytes have been read. Reset
+ * {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
+ *
+ * @param inputStream The input stream from which bytes should be read
+ * @param byteArray The {@code byte[]} into which bytes should be read
+ * @param totalBytes The total size of bytes to be read
+ * @return One of the {@code RESULT_*} flags defined in this class
+ */
+ private int readBytesInternal(
+ NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
+ if (bytesState == STATE_BEGIN_READING && totalBytes > byteArray.length) {
+ throw new IllegalArgumentException("Byte array not large enough");
+ }
+ if (bytesState >= totalBytes) {
+ return READ_RESULT_CONTINUE;
+ }
+ int remainingBytes = totalBytes - bytesState;
+ int additionalBytesRead = inputStream.read(byteArray, bytesState, remainingBytes);
+ return updateBytesState(additionalBytesRead, totalBytes);
+ }
+
+ /**
+ * Skips a set amount of bytes such that reading can be stopped and started again later if
+ * not enough bytes are available.
+ *
+ * Returns {@link #READ_RESULT_CONTINUE} if all bytes have been skipped. Reset
+ * {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes
+ * should be skipped.
+ *
+ * @param inputStream The input stream from which bytes should be skipped
+ * @param totalBytes The total size of bytes to be skipped
+ * @return One of the {@code RESULT_*} flags defined in this class
+ */
+ private int skipBytesInternal(NonBlockingInputStream inputStream, int totalBytes) {
+ if (bytesState >= totalBytes) {
+ return READ_RESULT_CONTINUE;
+ }
+ int remainingBytes = totalBytes - bytesState;
+ int additionalBytesRead = inputStream.skip(remainingBytes);
+ return updateBytesState(additionalBytesRead, totalBytes);
+ }
+
+ /**
+ * Updates {@link #bytesState} and {@link #bytesRead} after reading bytes in one of the
+ * {@code verbBytesInternal} methods.
+ *
+ * @param additionalBytesRead The number of additional bytes read to be accounted for
+ * @param totalBytes The total size of bytes to be read or skipped
+ * @return One of the {@code RESULT_*} flags defined in this class
+ */
+ private int updateBytesState(int additionalBytesRead, int totalBytes) {
+ if (additionalBytesRead == -1) {
+ return READ_RESULT_END_OF_FILE;
+ }
+ bytesState += additionalBytesRead;
+ bytesRead += additionalBytesRead;
+ if (bytesState < totalBytes) {
+ return READ_RESULT_NEED_MORE_DATA;
+ } else {
+ return READ_RESULT_CONTINUE;
+ }
+ }
+
+ /**
+ * Parses and returns the integer value currently read into the first {@code byteLength} bytes
+ * of {@link #tempByteArray}. EBML varint length masks can optionally be removed.
+ *
+ * @param byteLength The number of bytes to parse from {@link #tempByteArray}
+ * @param removeLengthMask Removes the variable-length integer length mask from the value
+ * @return The resulting integer value. This value could be up to 8-bytes so a Java long is used
+ */
+ private long getTempByteArrayValue(int byteLength, boolean removeLengthMask) {
+ if (removeLengthMask) {
+ tempByteArray[0] &= ~VARINT_LENGTH_MASKS[varintBytesLength - 1];
+ }
+ long varint = 0;
+ for (int i = 0; i < byteLength; i++) {
+ // Shift all existing bits up one byte and add the next byte at the bottom.
+ varint = (varint << 8) | (tempByteArray[i] & 0xff);
+ }
+ return varint;
+ }
+
+ /**
+ * Used in {@link #masterElementsStack} to track when the current master element ends so that
+ * {@link EbmlEventHandler#onMasterElementEnd(int)} is called.
+ */
+ private static final class MasterElement {
+
+ private final int elementId;
+ private final long elementEndOffsetBytes;
+
+ private MasterElement(int elementId, long elementEndOffsetBytes) {
+ this.elementId = elementId;
+ this.elementEndOffsetBytes = elementEndOffsetBytes;
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java
new file mode 100644
index 0000000000..351eff32d9
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java
@@ -0,0 +1,427 @@
+/*
+ * 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.parser.webm;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.parser.SegmentIndex;
+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.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Default version of an extractor to facilitate data retrieval from the WebM container format.
+ *
+ * WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
+ * Matroska is available here.
+ * More info about WebM is here.
+ */
+@TargetApi(16)
+public final class DefaultWebmExtractor implements WebmExtractor {
+
+ private static final String DOC_TYPE_WEBM = "webm";
+ private static final String CODEC_ID_VP9 = "V_VP9";
+ private static final int UNKNOWN = -1;
+
+ // Element IDs
+ private static final int ID_EBML = 0x1A45DFA3;
+ private static final int ID_EBML_READ_VERSION = 0x42F7;
+ private static final int ID_DOC_TYPE = 0x4282;
+ private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
+
+ private static final int ID_SEGMENT = 0x18538067;
+
+ private static final int ID_INFO = 0x1549A966;
+ private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
+ private static final int ID_DURATION = 0x4489;
+
+ private static final int ID_CLUSTER = 0x1F43B675;
+ private static final int ID_TIME_CODE = 0xE7;
+ private static final int ID_SIMPLE_BLOCK = 0xA3;
+
+ private static final int ID_TRACKS = 0x1654AE6B;
+ private static final int ID_TRACK_ENTRY = 0xAE;
+ private static final int ID_CODEC_ID = 0x86;
+ private static final int ID_VIDEO = 0xE0;
+ private static final int ID_PIXEL_WIDTH = 0xB0;
+ private static final int ID_PIXEL_HEIGHT = 0xBA;
+
+ private static final int ID_CUES = 0x1C53BB6B;
+ private static final int ID_CUE_POINT = 0xBB;
+ private static final int ID_CUE_TIME = 0xB3;
+ private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
+ private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
+
+ // SimpleBlock Lacing Values
+ private static final int LACING_NONE = 0;
+ private static final int LACING_XIPH = 1;
+ private static final int LACING_FIXED = 2;
+ private static final int LACING_EBML = 3;
+
+ private final EbmlReader reader;
+ private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
+
+ private SampleHolder tempSampleHolder;
+ private boolean sampleRead;
+
+ private boolean prepared = false;
+ private long segmentStartOffsetBytes = UNKNOWN;
+ private long segmentEndOffsetBytes = UNKNOWN;
+ private long timecodeScale = 1000000L;
+ private long durationUs = UNKNOWN;
+ private int pixelWidth = UNKNOWN;
+ private int pixelHeight = UNKNOWN;
+ private long cuesSizeBytes = UNKNOWN;
+ private long clusterTimecodeUs = UNKNOWN;
+ private long simpleBlockTimecodeUs = UNKNOWN;
+ private MediaFormat format;
+ private SegmentIndex cues;
+ private LongArray cueTimesUs;
+ private LongArray cueClusterPositions;
+
+ public DefaultWebmExtractor() {
+ this(new DefaultEbmlReader());
+ }
+
+ /* package */ DefaultWebmExtractor(EbmlReader reader) {
+ this.reader = reader;
+ this.reader.setEventHandler(new InnerEbmlEventHandler());
+ this.cueTimesUs = new LongArray();
+ this.cueClusterPositions = new LongArray();
+ }
+
+ @Override
+ public boolean isPrepared() {
+ return prepared;
+ }
+
+ @Override
+ public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
+ tempSampleHolder = sampleHolder;
+ sampleRead = false;
+ reader.read(inputStream);
+ tempSampleHolder = null;
+ return sampleRead;
+ }
+
+ @Override
+ public boolean seekTo(long seekTimeUs, boolean allowNoop) {
+ checkPrepared();
+ if (allowNoop
+ && simpleBlockTimecodeUs != UNKNOWN
+ && seekTimeUs >= simpleBlockTimecodeUs) {
+ int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
+ if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
+ return false;
+ }
+ }
+ reader.reset();
+ return true;
+ }
+
+ @Override
+ public SegmentIndex getCues() {
+ checkPrepared();
+ return cues;
+ }
+
+ @Override
+ public MediaFormat getFormat() {
+ checkPrepared();
+ return format;
+ }
+
+ /* package */ int getElementType(int id) {
+ switch (id) {
+ case ID_EBML:
+ case ID_SEGMENT:
+ case ID_INFO:
+ case ID_CLUSTER:
+ case ID_TRACKS:
+ case ID_TRACK_ENTRY:
+ case ID_VIDEO:
+ case ID_CUES:
+ case ID_CUE_POINT:
+ case ID_CUE_TRACK_POSITIONS:
+ return EbmlReader.TYPE_MASTER;
+ case ID_EBML_READ_VERSION:
+ case ID_DOC_TYPE_READ_VERSION:
+ case ID_TIMECODE_SCALE:
+ case ID_TIME_CODE:
+ case ID_PIXEL_WIDTH:
+ case ID_PIXEL_HEIGHT:
+ case ID_CUE_TIME:
+ case ID_CUE_CLUSTER_POSITION:
+ return EbmlReader.TYPE_UNSIGNED_INT;
+ case ID_DOC_TYPE:
+ case ID_CODEC_ID:
+ return EbmlReader.TYPE_STRING;
+ case ID_SIMPLE_BLOCK:
+ return EbmlReader.TYPE_BINARY;
+ case ID_DURATION:
+ return EbmlReader.TYPE_FLOAT;
+ default:
+ return EbmlReader.TYPE_UNKNOWN;
+ }
+ }
+
+ /* package */ boolean onMasterElementStart(
+ int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
+ switch (id) {
+ case ID_SEGMENT:
+ if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) {
+ throw new IllegalStateException("Multiple Segment elements not supported");
+ }
+ segmentStartOffsetBytes = elementOffsetBytes + headerSizeBytes;
+ segmentEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
+ break;
+ case ID_CUES:
+ cuesSizeBytes = headerSizeBytes + contentsSizeBytes;
+ break;
+ default:
+ // pass
+ }
+ return true;
+ }
+
+ /* package */ boolean onMasterElementEnd(int id) {
+ if (id == ID_CUES) {
+ finishPreparing();
+ return false;
+ }
+ return true;
+ }
+
+ /* package */ boolean onIntegerElement(int id, long value) {
+ switch (id) {
+ case ID_EBML_READ_VERSION:
+ // Validate that EBMLReadVersion is supported. This extractor only supports v1.
+ if (value != 1) {
+ throw new IllegalArgumentException("EBMLReadVersion " + value + " not supported");
+ }
+ break;
+ case ID_DOC_TYPE_READ_VERSION:
+ // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
+ if (value < 1 || value > 2) {
+ throw new IllegalArgumentException("DocTypeReadVersion " + value + " not supported");
+ }
+ break;
+ case ID_TIMECODE_SCALE:
+ timecodeScale = value;
+ break;
+ case ID_PIXEL_WIDTH:
+ pixelWidth = (int) value;
+ break;
+ case ID_PIXEL_HEIGHT:
+ pixelHeight = (int) value;
+ break;
+ case ID_CUE_TIME:
+ cueTimesUs.add(scaleTimecodeToUs(value));
+ break;
+ case ID_CUE_CLUSTER_POSITION:
+ cueClusterPositions.add(value);
+ break;
+ case ID_TIME_CODE:
+ clusterTimecodeUs = scaleTimecodeToUs(value);
+ break;
+ default:
+ // pass
+ }
+ return true;
+ }
+
+ /* package */ boolean onFloatElement(int id, double value) {
+ if (id == ID_DURATION) {
+ durationUs = scaleTimecodeToUs((long) value);
+ }
+ return true;
+ }
+
+ /* package */ boolean onStringElement(int id, String value) {
+ switch (id) {
+ case ID_DOC_TYPE:
+ // Validate that DocType is supported. This extractor only supports "webm".
+ if (!DOC_TYPE_WEBM.equals(value)) {
+ throw new IllegalArgumentException("DocType " + value + " not supported");
+ }
+ break;
+ case ID_CODEC_ID:
+ // Validate that CodecID is supported. This extractor only supports "V_VP9".
+ if (!CODEC_ID_VP9.equals(value)) {
+ throw new IllegalArgumentException("CodecID " + value + " not supported");
+ }
+ break;
+ default:
+ // pass
+ }
+ return true;
+ }
+
+ /* package */ boolean onBinaryElement(
+ int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
+ NonBlockingInputStream inputStream) {
+ if (id == ID_SIMPLE_BLOCK) {
+ // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
+ // for info about how data is organized in a SimpleBlock element.
+
+ // Value of trackNumber is not used but needs to be read.
+ reader.readVarint(inputStream);
+
+ // Next three bytes have timecode and flags.
+ reader.readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
+
+ // First two bytes of the three are the relative timecode.
+ int timecode =
+ (simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
+ long timecodeUs = scaleTimecodeToUs(timecode);
+
+ // Last byte of the three has some flags and the lacing value.
+ boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
+ boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
+ int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
+
+ // Validate lacing and set info into sample holder.
+ switch (lacing) {
+ case LACING_NONE:
+ long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
+ simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
+ tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
+ tempSampleHolder.decodeOnly = invisible;
+ tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
+ tempSampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
+ break;
+ case LACING_EBML:
+ case LACING_FIXED:
+ case LACING_XIPH:
+ default:
+ throw new IllegalStateException("Lacing mode " + lacing + " not supported");
+ }
+
+ // Read video data into sample holder.
+ reader.readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size);
+ sampleRead = true;
+ return false;
+ } else {
+ reader.skipBytes(inputStream, contentsSizeBytes);
+ return true;
+ }
+ }
+
+ private long scaleTimecodeToUs(long unscaledTimecode) {
+ return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
+ }
+
+ private void checkPrepared() {
+ if (!prepared) {
+ throw new IllegalStateException("Parser not yet prepared");
+ }
+ }
+
+ private void finishPreparing() {
+ if (prepared) {
+ throw new IllegalStateException("Already prepared");
+ } else if (segmentStartOffsetBytes == UNKNOWN) {
+ throw new IllegalStateException("Segment start/end offsets unknown");
+ } else if (durationUs == UNKNOWN) {
+ throw new IllegalStateException("Duration unknown");
+ } else if (pixelWidth == UNKNOWN || pixelHeight == UNKNOWN) {
+ throw new IllegalStateException("Pixel width/height unknown");
+ } else if (cuesSizeBytes == UNKNOWN) {
+ throw new IllegalStateException("Cues size unknown");
+ } else if (cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
+ throw new IllegalStateException("Invalid/missing cue points");
+ }
+
+ format = MediaFormat.createVideoFormat(
+ MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
+
+ int cuePointsSize = cueTimesUs.size();
+ int[] sizes = new int[cuePointsSize];
+ long[] offsets = new long[cuePointsSize];
+ long[] durationsUs = new long[cuePointsSize];
+ long[] timesUs = new long[cuePointsSize];
+ for (int i = 0; i < cuePointsSize; i++) {
+ timesUs[i] = cueTimesUs.get(i);
+ offsets[i] = segmentStartOffsetBytes + cueClusterPositions.get(i);
+ }
+ for (int i = 0; i < cuePointsSize - 1; i++) {
+ sizes[i] = (int) (offsets[i + 1] - offsets[i]);
+ durationsUs[i] = timesUs[i + 1] - timesUs[i];
+ }
+ sizes[cuePointsSize - 1] = (int) (segmentEndOffsetBytes - offsets[cuePointsSize - 1]);
+ durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
+ cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs);
+ cueTimesUs = null;
+ cueClusterPositions = null;
+
+ prepared = true;
+ }
+
+ /**
+ * Passes events through to {@link DefaultWebmExtractor} as
+ * callbacks from {@link EbmlReader} are received.
+ */
+ private final class InnerEbmlEventHandler implements EbmlEventHandler {
+
+ @Override
+ public int getElementType(int id) {
+ return DefaultWebmExtractor.this.getElementType(id);
+ }
+
+ @Override
+ public boolean onMasterElementStart(
+ int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
+ return DefaultWebmExtractor.this.onMasterElementStart(
+ id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
+ }
+
+ @Override
+ public boolean onMasterElementEnd(int id) {
+ return DefaultWebmExtractor.this.onMasterElementEnd(id);
+ }
+
+ @Override
+ public boolean onIntegerElement(int id, long value) {
+ return DefaultWebmExtractor.this.onIntegerElement(id, value);
+ }
+
+ @Override
+ public boolean onFloatElement(int id, double value) {
+ return DefaultWebmExtractor.this.onFloatElement(id, value);
+ }
+
+ @Override
+ public boolean onStringElement(int id, String value) {
+ return DefaultWebmExtractor.this.onStringElement(id, value);
+ }
+
+ @Override
+ public boolean onBinaryElement(
+ int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
+ NonBlockingInputStream inputStream) {
+ return DefaultWebmExtractor.this.onBinaryElement(
+ id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes, inputStream);
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java
new file mode 100644
index 0000000000..42e26d4531
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java
@@ -0,0 +1,120 @@
+/*
+ * 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.parser.webm;
+
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Defines EBML element IDs/types and reacts to events.
+ */
+/* package */ interface EbmlEventHandler {
+
+ /**
+ * Retrieves the type of an element ID.
+ *
+ * If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped.
+ * Note that all children of a skipped master element are also skipped.
+ *
+ * @param id The integer ID of this element
+ * @return One of the {@code TYPE_} constants defined in this class
+ */
+ public int getElementType(int id);
+
+ /**
+ * Called when a master element is encountered in the {@link NonBlockingInputStream}.
+ *
+ * Following events should be considered as taking place "within" this element until a
+ * matching call to {@link #onMasterElementEnd(int)} is made. Note that it is possible for
+ * another master element of the same ID to be nested within itself.
+ *
+ * @param id The integer ID of this element
+ * @param elementOffsetBytes The byte offset where this element starts
+ * @param headerSizeBytes The byte length of this element's ID and size header
+ * @param contentsSizeBytes The byte length of this element's children
+ * @return {@code false} if parsing should stop right away
+ */
+ public boolean onMasterElementStart(
+ int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes);
+
+ /**
+ * Called when a master element has finished reading in all of its children from the
+ * {@link NonBlockingInputStream}.
+ *
+ * @param id The integer ID of this element
+ * @return {@code false} if parsing should stop right away
+ */
+ public boolean onMasterElementEnd(int id);
+
+ /**
+ * Called when an integer element is encountered in the {@link NonBlockingInputStream}.
+ *
+ * @param id The integer ID of this element
+ * @param value The integer value this element contains
+ * @return {@code false} if parsing should stop right away
+ */
+ public boolean onIntegerElement(int id, long value);
+
+ /**
+ * Called when a float element is encountered in the {@link NonBlockingInputStream}.
+ *
+ * @param id The integer ID of this element
+ * @param value The float value this element contains
+ * @return {@code false} if parsing should stop right away
+ */
+ public boolean onFloatElement(int id, double value);
+
+ /**
+ * Called when a string element is encountered in the {@link NonBlockingInputStream}.
+ *
+ * @param id The integer ID of this element
+ * @param value The string value this element contains
+ * @return {@code false} if parsing should stop right away
+ */
+ public boolean onStringElement(int id, String value);
+
+ /**
+ * Called when a binary element is encountered in the {@link NonBlockingInputStream}.
+ *
+ * The element header (containing element ID and content size) will already have been read.
+ * Subclasses must exactly read the entire contents of the element, which is
+ * {@code contentsSizeBytes} in length. It's guaranteed that the full element contents will be
+ * immediately available from {@code inputStream}.
+ *
+ * Several methods in {@link EbmlReader} are available for reading the contents of a
+ * binary element:
+ * EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
* It was originally designed for the Matroska container format. More information about EBML and
* Matroska is available here.
*/
-public abstract class EbmlReader {
+/* package */ interface EbmlReader {
// Element Types
- protected static final int TYPE_UNKNOWN = 0; // Undefined element.
- protected static final int TYPE_MASTER = 1; // Contains child elements.
- protected static final int TYPE_UNSIGNED_INT = 2;
- protected static final int TYPE_STRING = 3;
- protected static final int TYPE_BINARY = 4;
- protected static final int TYPE_FLOAT = 5;
+ /** Undefined element. */
+ public static final int TYPE_UNKNOWN = 0;
+ /** Contains child elements. */
+ public static final int TYPE_MASTER = 1;
+ /** Unsigned integer value of up to 8 bytes. */
+ public static final int TYPE_UNSIGNED_INT = 2;
+ public static final int TYPE_STRING = 3;
+ public static final int TYPE_BINARY = 4;
+ /** IEEE floating point value of either 4 or 8 bytes. */
+ public static final int TYPE_FLOAT = 5;
- // Return values for methods read, readElementId, readElementSize, readVarintBytes, and readBytes.
- protected static final int RESULT_CONTINUE = 0;
- protected static final int RESULT_NEED_MORE_DATA = 1;
- protected static final int RESULT_END_OF_FILE = 2;
+ // Return values for reading methods.
+ public static final int READ_RESULT_CONTINUE = 0;
+ public static final int READ_RESULT_NEED_MORE_DATA = 1;
+ public static final int READ_RESULT_END_OF_FILE = 2;
- // State values used in variables state, elementIdState, elementContentSizeState, and
- // varintBytesState.
- private static final int STATE_BEGIN_READING = 0;
- private static final int STATE_READ_CONTENTS = 1;
- private static final int STATE_FINISHED_READING = 2;
-
- /**
- * The first byte of a variable-length integer (varint) will have one of these bit masks
- * indicating the total length in bytes. {@code 0x80} is a one-byte integer,
- * {@code 0x40} is two bytes, and so on up to eight bytes.
- */
- private static final int[] VARINT_LENGTH_MASKS = new int[] {
- 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
- };
-
- private final Stack Several methods are available for reading the contents of a binary element:
- * This includes resetting the value returned from {@link #getBytesRead()} to 0 and discarding
+ * all pending {@link EbmlEventHandler#onMasterElementEnd(int)} events.
*/
- protected final void reset() {
- prepareForNextElement();
- masterElementsStack.clear();
- bytesRead = 0;
- }
+ public void reset();
/**
* Reads, parses, and returns an EBML variable-length integer (varint) from the contents
* of a binary element.
*
- * @param inputStream The input stream from which data should be read.
- * @return The varint value at the current position of the contents of a binary element.
+ * @param inputStream The input stream from which data should be read
+ * @return The varint value at the current position of the contents of a binary element
*/
- protected final long readVarint(NonBlockingInputStream inputStream) {
- varintBytesState = STATE_BEGIN_READING;
- Assertions.checkState(readVarintBytes(inputStream) == RESULT_CONTINUE);
- return parseTempByteArray(varintBytesLength, true);
- }
+ public long readVarint(NonBlockingInputStream inputStream);
/**
* Reads a fixed number of bytes from the contents of a binary element into a {@link ByteBuffer}.
*
- * @param inputStream The input stream from which data should be read.
- * @param byteBuffer The {@link ByteBuffer} to which data should be written.
- * @param totalBytes The fixed number of bytes to be read and written.
+ * @param inputStream The input stream from which data should be read
+ * @param byteBuffer The {@link ByteBuffer} to which data should be written
+ * @param totalBytes The fixed number of bytes to be read and written
*/
- protected final void readBytes(
- NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
- bytesState = 0;
- Assertions.checkState(readBytes(inputStream, byteBuffer, null, totalBytes) == RESULT_CONTINUE);
- }
+ public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes);
/**
* Reads a fixed number of bytes from the contents of a binary element into a {@code byte[]}.
*
- * @param inputStream The input stream from which data should be read.
- * @param byteArray The byte array to which data should be written.
- * @param totalBytes The fixed number of bytes to be read and written.
+ * @param inputStream The input stream from which data should be read
+ * @param byteArray The byte array to which data should be written
+ * @param totalBytes The fixed number of bytes to be read and written
*/
- protected final void readBytes(
- NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
- bytesState = 0;
- Assertions.checkState(readBytes(inputStream, null, byteArray, totalBytes) == RESULT_CONTINUE);
- }
+ public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes);
/**
* Skips a fixed number of bytes from the contents of a binary element.
*
- * @param inputStream The input stream from which data should be skipped.
- * @param totalBytes The fixed number of bytes to be skipped.
+ * @param inputStream The input stream from which data should be skipped
+ * @param totalBytes The fixed number of bytes to be skipped
*/
- protected final void skipBytes(NonBlockingInputStream inputStream, int totalBytes) {
- bytesState = 0;
- Assertions.checkState(readBytes(inputStream, null, null, totalBytes) == RESULT_CONTINUE);
- }
-
- /**
- * Resets the internal state of {@link #read(NonBlockingInputStream)} so that it can start
- * reading a new element from scratch.
- */
- private final void prepareForNextElement() {
- state = STATE_BEGIN_READING;
- elementIdState = STATE_BEGIN_READING;
- elementContentSizeState = STATE_BEGIN_READING;
- elementOffset = bytesRead;
- }
-
- /**
- * Reads an element ID such that reading can be stopped and started again in a later call
- * if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if a full element ID
- * has been read into {@link #elementId}. Reset {@link #elementIdState} to
- * {@link #STATE_BEGIN_READING} before calling to indicate a new element ID should be read.
- *
- * @param inputStream The input stream from which an element ID should be read.
- * @return One of the {@code RESULT_*} flags defined in this class.
- */
- private int readElementId(NonBlockingInputStream inputStream) {
- if (elementIdState == STATE_FINISHED_READING) {
- return RESULT_CONTINUE;
- }
- if (elementIdState == STATE_BEGIN_READING) {
- varintBytesState = STATE_BEGIN_READING;
- elementIdState = STATE_READ_CONTENTS;
- }
- final int result = readVarintBytes(inputStream);
- if (result != RESULT_CONTINUE) {
- return result;
- }
- elementId = (int) parseTempByteArray(varintBytesLength, false);
- elementIdState = STATE_FINISHED_READING;
- return RESULT_CONTINUE;
- }
-
- /**
- * Reads an element's content size such that reading can be stopped and started again in a later
- * call if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if an entire element
- * size has been read into {@link #elementContentSize}. Reset {@link #elementContentSizeState} to
- * {@link #STATE_BEGIN_READING} before calling to indicate a new element size should be read.
- *
- * @param inputStream The input stream from which an element size should be read.
- * @return One of the {@code RESULT_*} flags defined in this class.
- */
- private int readElementContentSize(NonBlockingInputStream inputStream) {
- if (elementContentSizeState == STATE_FINISHED_READING) {
- return RESULT_CONTINUE;
- }
- if (elementContentSizeState == STATE_BEGIN_READING) {
- varintBytesState = STATE_BEGIN_READING;
- elementContentSizeState = STATE_READ_CONTENTS;
- }
- final int result = readVarintBytes(inputStream);
- if (result != RESULT_CONTINUE) {
- return result;
- }
- elementContentSize = parseTempByteArray(varintBytesLength, true);
- elementContentSizeState = STATE_FINISHED_READING;
- return RESULT_CONTINUE;
- }
-
- /**
- * Reads an EBML variable-length integer (varint) such that reading can be stopped and started
- * again in a later call if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if
- * an entire varint has been read into {@link #tempByteArray} and the length of the varint is in
- * {@link #varintBytesLength}. Reset {@link #varintBytesState} to {@link #STATE_BEGIN_READING}
- * before calling to indicate a new varint should be read.
- *
- * @param inputStream The input stream from which a varint should be read.
- * @return One of the {@code RESULT_*} flags defined in this class.
- */
- private int readVarintBytes(NonBlockingInputStream inputStream) {
- if (varintBytesState == STATE_FINISHED_READING) {
- return RESULT_CONTINUE;
- }
-
- // Read first byte to get length.
- if (varintBytesState == STATE_BEGIN_READING) {
- bytesState = 0;
- final int result = readBytes(inputStream, null, tempByteArray, 1);
- if (result != RESULT_CONTINUE) {
- return result;
- }
- varintBytesState = STATE_READ_CONTENTS;
-
- final int firstByte = tempByteArray[0] & 0xff;
- varintBytesLength = -1;
- for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
- if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
- varintBytesLength = i + 1;
- break;
- }
- }
- if (varintBytesLength == -1) {
- throw new IllegalStateException(
- "No valid varint length mask found at bytesRead = " + bytesRead);
- }
- }
-
- // Read remaining bytes.
- final int result = readBytes(inputStream, null, tempByteArray, varintBytesLength);
- if (result != RESULT_CONTINUE) {
- return result;
- }
-
- // All bytes have been read.
- return RESULT_CONTINUE;
- }
-
- /**
- * Reads a set amount of bytes into a {@link ByteBuffer}, {@code byte[]}, or nowhere (skipping
- * the bytes) such that reading can be stopped and started again later if not enough bytes are
- * available. Returns {@link #RESULT_CONTINUE} if all bytes have been read. Reset
- * {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
- *
- * If both {@code byteBuffer} and {@code byteArray} are not null then bytes are only read
- * into {@code byteBuffer}.
- *
- * @param inputStream The input stream from which bytes should be read.
- * @param byteBuffer The optional {@link ByteBuffer} into which bytes should be read.
- * @param byteArray The optional {@code byte[]} into which bytes should be read.
- * @param totalBytes The total size of bytes to be read or skipped.
- * @return One of the {@code RESULT_*} flags defined in this class.
- */
- private int readBytes(
- NonBlockingInputStream inputStream, ByteBuffer byteBuffer, byte[] byteArray, int totalBytes) {
- if (bytesState == STATE_BEGIN_READING
- && ((byteBuffer != null && totalBytes > byteBuffer.capacity())
- || (byteArray != null && totalBytes > byteArray.length))) {
- throw new IllegalStateException("Byte destination not large enough");
- }
- if (bytesState < totalBytes) {
- final int remainingBytes = totalBytes - bytesState;
- final int result;
- if (byteBuffer != null) {
- result = inputStream.read(byteBuffer, remainingBytes);
- } else if (byteArray != null) {
- result = inputStream.read(byteArray, bytesState, remainingBytes);
- } else {
- result = inputStream.skip(remainingBytes);
- }
- if (result == -1) {
- return RESULT_END_OF_FILE;
- }
- bytesState += result;
- bytesRead += result;
- if (bytesState < totalBytes) {
- return RESULT_NEED_MORE_DATA;
- }
- }
- return RESULT_CONTINUE;
- }
-
- /**
- * Parses and returns the integer value currently read into the first {@code byteLength} bytes
- * of {@link #tempByteArray}. EBML varint length masks can optionally be removed.
- *
- * @param byteLength The number of bytes to parse from {@link #tempByteArray}.
- * @param removeLengthMask Removes the variable-length integer length mask from the value.
- * @return The resulting integer value. This value could be up to 8-bytes so a Java long is used.
- */
- private long parseTempByteArray(int byteLength, boolean removeLengthMask) {
- if (removeLengthMask) {
- tempByteArray[0] &= ~VARINT_LENGTH_MASKS[varintBytesLength - 1];
- }
- long varint = 0;
- for (int i = 0; i < byteLength; i++) {
- // Shift all existing bits up one byte and add the next byte at the bottom.
- varint = (varint << 8) | (tempByteArray[i] & 0xff);
- }
- return varint;
- }
-
- /**
- * Used in {@link #masterElementsStack} to track when the current master element ends so that
- * {@link #onMasterElementEnd(int)} is called.
- */
- private static final class MasterElement {
-
- private final int elementId;
- private final long elementEndOffset;
-
- private MasterElement(int elementId, long elementEndOffset) {
- this.elementId = elementId;
- this.elementEndOffset = elementEndOffset;
- }
-
- }
+ public void skipBytes(NonBlockingInputStream inputStream, int totalBytes);
}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java
index 49b82f4a16..4ecefe7906 100644
--- a/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java
+++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java
@@ -19,97 +19,22 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.SegmentIndex;
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.util.Arrays;
/**
- * Facilitates the extraction of data from the WebM container format with a
- * non-blocking, incremental parser based on {@link EbmlReader}.
+ * Extractor to facilitate data retrieval from the WebM container format.
*
* WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
* Matroska is available here.
* More info about WebM is here.
*/
-@TargetApi(16)
-public final class WebmExtractor extends EbmlReader {
-
- private static final String DOC_TYPE_WEBM = "webm";
- private static final String CODEC_ID_VP9 = "V_VP9";
- private static final int UNKNOWN = -1;
-
- // Element IDs
- private static final int ID_EBML = 0x1A45DFA3;
- private static final int ID_EBML_READ_VERSION = 0x42F7;
- private static final int ID_DOC_TYPE = 0x4282;
- private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
-
- private static final int ID_SEGMENT = 0x18538067;
-
- private static final int ID_INFO = 0x1549A966;
- private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
- private static final int ID_DURATION = 0x4489;
-
- private static final int ID_CLUSTER = 0x1F43B675;
- private static final int ID_TIME_CODE = 0xE7;
- private static final int ID_SIMPLE_BLOCK = 0xA3;
-
- private static final int ID_TRACKS = 0x1654AE6B;
- private static final int ID_TRACK_ENTRY = 0xAE;
- private static final int ID_CODEC_ID = 0x86;
- private static final int ID_VIDEO = 0xE0;
- private static final int ID_PIXEL_WIDTH = 0xB0;
- private static final int ID_PIXEL_HEIGHT = 0xBA;
-
- private static final int ID_CUES = 0x1C53BB6B;
- private static final int ID_CUE_POINT = 0xBB;
- private static final int ID_CUE_TIME = 0xB3;
- private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
- private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
-
- // SimpleBlock Lacing Values
- private static final int LACING_NONE = 0;
- private static final int LACING_XIPH = 1;
- private static final int LACING_FIXED = 2;
- private static final int LACING_EBML = 3;
-
- private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
-
- private SampleHolder tempSampleHolder;
- private boolean sampleRead;
-
- private boolean prepared = false;
- private long segmentStartPosition = UNKNOWN;
- private long segmentEndPosition = UNKNOWN;
- private long timecodeScale = 1000000L;
- private long durationUs = UNKNOWN;
- private int pixelWidth = UNKNOWN;
- private int pixelHeight = UNKNOWN;
- private int cuesByteSize = UNKNOWN;
- private long clusterTimecodeUs = UNKNOWN;
- private long simpleBlockTimecodeUs = UNKNOWN;
- private MediaFormat format;
- private SegmentIndex cues;
- private LongArray cueTimesUs;
- private LongArray cueClusterPositions;
-
- public WebmExtractor() {
- cueTimesUs = new LongArray();
- cueClusterPositions = new LongArray();
- }
+public interface WebmExtractor {
/**
* Whether the has parsed the cues and sample format from the stream.
*
- * @return True if the extractor is prepared. False otherwise.
+ * @return True if the extractor is prepared. False otherwise
*/
- public boolean isPrepared() {
- return prepared;
- }
+ public boolean isPrepared();
/**
* Consumes data from a {@link NonBlockingInputStream}.
@@ -118,289 +43,36 @@ public final class WebmExtractor extends EbmlReader {
* {@code sampleHolder}. Hence the same {@link SampleHolder} instance must be passed
* in subsequent calls until the whole sample has been read.
*
- * @param inputStream The input stream from which data should be read.
- * @param sampleHolder A {@link SampleHolder} into which the sample should be read.
- * @return {@code true} if a sample has been read into the sample holder, otherwise {@code false}.
+ * @param inputStream The input stream from which data should be read
+ * @param sampleHolder A {@link SampleHolder} into which the sample should be read
+ * @return {@code true} if a sample has been read into the sample holder
*/
- public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
- tempSampleHolder = sampleHolder;
- sampleRead = false;
- super.read(inputStream);
- tempSampleHolder = null;
- return sampleRead;
- }
+ public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder);
/**
* Seeks to a position before or equal to the requested time.
*
- * @param seekTimeUs The desired seek time in microseconds.
+ * @param seekTimeUs The desired seek time in microseconds
* @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
* segment, is equal to or greater than the time of the current sample, and if there does not
- * exist a sync frame between these two times.
- * @return True if the operation resulted in a change of state. False if it was a no-op.
+ * exist a sync frame between these two times
+ * @return True if the operation resulted in a change of state. False if it was a no-op
*/
- public boolean seekTo(long seekTimeUs, boolean allowNoop) {
- checkPrepared();
- if (allowNoop && simpleBlockTimecodeUs != UNKNOWN && seekTimeUs >= simpleBlockTimecodeUs) {
- final int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
- if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
- return false;
- }
- }
- reset();
- return true;
- }
+ public boolean seekTo(long seekTimeUs, boolean allowNoop);
/**
* Returns the cues for the media stream.
*
* @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
- * prepared.
+ * prepared
*/
- public SegmentIndex getCues() {
- checkPrepared();
- return cues;
- }
+ public SegmentIndex getCues();
/**
* Returns the format of the samples contained within the media stream.
*
- * @return The sample media format, or null if the extracted is not yet prepared.
+ * @return The sample media format, or null if the extracted is not yet prepared
*/
- public MediaFormat getFormat() {
- checkPrepared();
- return format;
- }
-
- @Override
- protected int getElementType(int id) {
- switch (id) {
- case ID_EBML:
- case ID_SEGMENT:
- case ID_INFO:
- case ID_CLUSTER:
- case ID_TRACKS:
- case ID_TRACK_ENTRY:
- case ID_VIDEO:
- case ID_CUES:
- case ID_CUE_POINT:
- case ID_CUE_TRACK_POSITIONS:
- return EbmlReader.TYPE_MASTER;
- case ID_EBML_READ_VERSION:
- case ID_DOC_TYPE_READ_VERSION:
- case ID_TIMECODE_SCALE:
- case ID_TIME_CODE:
- case ID_PIXEL_WIDTH:
- case ID_PIXEL_HEIGHT:
- case ID_CUE_TIME:
- case ID_CUE_CLUSTER_POSITION:
- return EbmlReader.TYPE_UNSIGNED_INT;
- case ID_DOC_TYPE:
- case ID_CODEC_ID:
- return EbmlReader.TYPE_STRING;
- case ID_SIMPLE_BLOCK:
- return EbmlReader.TYPE_BINARY;
- case ID_DURATION:
- return EbmlReader.TYPE_FLOAT;
- default:
- return EbmlReader.TYPE_UNKNOWN;
- }
- }
-
- @Override
- protected boolean onMasterElementStart(
- int id, long elementOffset, int headerSize, int contentsSize) {
- switch (id) {
- case ID_SEGMENT:
- if (segmentStartPosition != UNKNOWN || segmentEndPosition != UNKNOWN) {
- throw new IllegalStateException("Multiple Segment elements not supported");
- }
- segmentStartPosition = elementOffset + headerSize;
- segmentEndPosition = elementOffset + headerSize + contentsSize;
- break;
- case ID_CUES:
- cuesByteSize = headerSize + contentsSize;
- break;
- }
- return true;
- }
-
- @Override
- protected boolean onMasterElementEnd(int id) {
- switch (id) {
- case ID_CUES:
- finishPreparing();
- return false;
- }
- return true;
- }
-
- @Override
- protected boolean onIntegerElement(int id, long value) {
- switch (id) {
- case ID_EBML_READ_VERSION:
- // Validate that EBMLReadVersion is supported. This extractor only supports v1.
- if (value != 1) {
- throw new IllegalStateException("EBMLReadVersion " + value + " not supported");
- }
- break;
- case ID_DOC_TYPE_READ_VERSION:
- // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
- if (value < 1 || value > 2) {
- throw new IllegalStateException("DocTypeReadVersion " + value + " not supported");
- }
- break;
- case ID_TIMECODE_SCALE:
- timecodeScale = value;
- break;
- case ID_PIXEL_WIDTH:
- pixelWidth = (int) value;
- break;
- case ID_PIXEL_HEIGHT:
- pixelHeight = (int) value;
- break;
- case ID_CUE_TIME:
- cueTimesUs.add(scaleTimecodeToUs(value));
- break;
- case ID_CUE_CLUSTER_POSITION:
- cueClusterPositions.add(value);
- break;
- case ID_TIME_CODE:
- clusterTimecodeUs = scaleTimecodeToUs(value);
- break;
- }
- return true;
- }
-
- @Override
- protected boolean onFloatElement(int id, double value) {
- switch (id) {
- case ID_DURATION:
- durationUs = scaleTimecodeToUs(value);
- break;
- }
- return true;
- }
-
- @Override
- protected boolean onStringElement(int id, String value) {
- switch (id) {
- case ID_DOC_TYPE:
- // Validate that DocType is supported. This extractor only supports "webm".
- if (!DOC_TYPE_WEBM.equals(value)) {
- throw new IllegalStateException("DocType " + value + " not supported");
- }
- break;
- case ID_CODEC_ID:
- // Validate that CodecID is supported. This extractor only supports "V_VP9".
- if (!CODEC_ID_VP9.equals(value)) {
- throw new IllegalStateException("CodecID " + value + " not supported");
- }
- break;
- }
- return true;
- }
-
- @Override
- protected boolean onBinaryElement(NonBlockingInputStream inputStream,
- int id, long elementOffset, int headerSize, int contentsSize) {
- switch (id) {
- case ID_SIMPLE_BLOCK:
- // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
- // for info about how data is organized in a SimpleBlock element.
-
- // Value of trackNumber is not used but needs to be read.
- readVarint(inputStream);
-
- // Next three bytes have timecode and flags.
- readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
-
- // First two bytes of the three are the relative timecode.
- final int timecode =
- (simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
- final long timecodeUs = scaleTimecodeToUs(timecode);
-
- // Last byte of the three has some flags and the lacing value.
- final boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
- final boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
- final int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
- //final boolean discardable = (simpleBlockTimecodeAndFlags[2] & 0x01) == 0x01; // Not used.
-
- // Validate lacing and set info into sample holder.
- switch (lacing) {
- case LACING_NONE:
- final long elementEndOffset = elementOffset + headerSize + contentsSize;
- simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
- tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
- tempSampleHolder.decodeOnly = invisible;
- tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
- tempSampleHolder.size = (int) (elementEndOffset - getBytesRead());
- break;
- case LACING_EBML:
- case LACING_FIXED:
- case LACING_XIPH:
- default:
- throw new IllegalStateException("Lacing mode " + lacing + " not supported");
- }
-
- // Read video data into sample holder.
- readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size);
- sampleRead = true;
- return false;
- default:
- skipBytes(inputStream, contentsSize);
- }
- return true;
- }
-
- private long scaleTimecodeToUs(long unscaledTimecode) {
- return (unscaledTimecode * timecodeScale) / 1000L;
- }
-
- private long scaleTimecodeToUs(double unscaledTimecode) {
- return (long) ((unscaledTimecode * timecodeScale) / 1000.0);
- }
-
- private void checkPrepared() {
- if (!prepared) {
- throw new IllegalStateException("Parser not yet prepared");
- }
- }
-
- private void finishPreparing() {
- if (prepared
- || segmentStartPosition == UNKNOWN || segmentEndPosition == UNKNOWN
- || durationUs == UNKNOWN
- || pixelWidth == UNKNOWN || pixelHeight == UNKNOWN
- || cuesByteSize == UNKNOWN
- || cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
- throw new IllegalStateException("Incorrect state in finishPreparing()");
- }
-
- format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth,
- pixelHeight, null);
-
- final int cuePointsSize = cueTimesUs.size();
- final int sizeBytes = cuesByteSize;
- final int[] sizes = new int[cuePointsSize];
- final long[] offsets = new long[cuePointsSize];
- final long[] durationsUs = new long[cuePointsSize];
- final long[] timesUs = new long[cuePointsSize];
- for (int i = 0; i < cuePointsSize; i++) {
- timesUs[i] = cueTimesUs.get(i);
- offsets[i] = segmentStartPosition + cueClusterPositions.get(i);
- }
- for (int i = 0; i < cuePointsSize - 1; i++) {
- sizes[i] = (int) (offsets[i + 1] - offsets[i]);
- durationsUs[i] = timesUs[i + 1] - timesUs[i];
- }
- sizes[cuePointsSize - 1] = (int) (segmentEndPosition - offsets[cuePointsSize - 1]);
- durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
- cues = new SegmentIndex(sizeBytes, sizes, offsets, durationsUs, timesUs);
- cueTimesUs = null;
- cueClusterPositions = null;
-
- prepared = true;
- }
+ public MediaFormat getFormat();
}
diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java
index ed8c1030fe..d6a70e0364 100644
--- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java
@@ -63,7 +63,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private final int maxHeight;
private final SparseArray
+ * The search is performed using a binary search algorithm, and so the array must be sorted.
+ *
+ * @param a The array to search.
+ * @param key The key being searched for.
+ * @param inclusive If the key is present in the array, whether to return the corresponding index.
+ * If false then the returned index corresponds to the largest value in the array that is
+ * strictly less than the key.
+ * @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than
+ * the smallest value in the array. If false then -1 will be returned.
+ */
+ public static int binarySearchFloor(long[] a, long key, boolean inclusive, boolean stayInBounds) {
+ int index = Arrays.binarySearch(a, key);
+ index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1));
+ return stayInBounds ? Math.max(0, index) : index;
+ }
+
+ /**
+ * Returns the index of the smallest value in an array that is greater than (or optionally equal
+ * to) a specified key.
+ *
+ * The search is performed using a binary search algorithm, and so the array must be sorted.
+ *
+ * @param a The array to search.
+ * @param key The key being searched for.
+ * @param inclusive If the key is present in the array, whether to return the corresponding index.
+ * If false then the returned index corresponds to the smallest value in the array that is
+ * strictly greater than the key.
+ * @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the
+ * key is greater than the largest value in the array. If false then {@code a.length} will be
+ * returned.
+ */
+ public static int binarySearchCeil(long[] a, long key, boolean inclusive, boolean stayInBounds) {
+ int index = Arrays.binarySearch(a, key);
+ index = index < 0 ? ~index : (inclusive ? index : (index + 1));
+ return stayInBounds ? Math.min(a.length - 1, index) : index;
+ }
+
+ /**
+ * Returns the index of the largest value in an list that is less than (or optionally equal to)
+ * a specified key.
+ *
+ * The search is performed using a binary search algorithm, and so the list must be sorted.
+ *
+ * @param list The list to search.
+ * @param key The key being searched for.
+ * @param inclusive If the key is present in the list, whether to return the corresponding index.
+ * If false then the returned index corresponds to the largest value in the list that is
+ * strictly less than the key.
+ * @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than
+ * the smallest value in the list. If false then -1 will be returned.
+ */
+ public static
+ * The search is performed using a binary search algorithm, and so the list must be sorted.
+ *
+ * @param list The list to search.
+ * @param key The key being searched for.
+ * @param inclusive If the key is present in the list, whether to return the corresponding index.
+ * If false then the returned index corresponds to the smallest value in the list that is
+ * strictly greater than the key.
+ * @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that
+ * the key is greater than the largest value in the list. If false then {@code list.size()}
+ * will be returned.
+ */
+ public static
+ *
+ *
+ * @param baseUri An uri that can form the base of the uri defined by the instance.
+ * @param stringUri A relative or absolute uri in string form.
+ * @param start The (zero based) index of the first byte of the range.
+ * @param length The length of the range, or -1 to indicate that the range is unbounded.
+ */
+ public RangedUri(Uri baseUri, String stringUri, long start, long length) {
+ Assertions.checkArgument(baseUri != null || stringUri != null);
+ this.baseUri = baseUri;
+ this.stringUri = stringUri;
+ this.start = start;
+ this.length = length;
+ }
+
+ /**
+ * Returns the {@link Uri} represented by the instance.
+ *
+ * @return The {@link Uri} represented by the instance.
+ */
+ public Uri getUri() {
+ if (stringUri == null) {
+ return baseUri;
+ }
+ Uri uri = Uri.parse(stringUri);
+ if (!uri.isAbsolute() && baseUri != null) {
+ uri = Uri.withAppendedPath(baseUri, stringUri);
+ }
+ return uri;
+ }
+
+ /**
+ * Attempts to merge this {@link RangedUri} with another.
+ *
+ *
+ *
+ * @param id The integer ID of this element
+ * @param elementOffsetBytes The byte offset where this element starts
+ * @param headerSizeBytes The byte length of this element's ID and size header
+ * @param contentsSizeBytes The byte length of this element's contents
+ * @param inputStream The {@link NonBlockingInputStream} from which this
+ * element's contents should be read
+ * @return {@code false} if parsing should stop right away
+ */
+ public boolean onBinaryElement(
+ int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
+ NonBlockingInputStream inputStream);
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java
index 6dbc744e14..a9bf11f699 100644
--- a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java
+++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java
@@ -16,528 +16,92 @@
package com.google.android.exoplayer.parser.webm;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
-import com.google.android.exoplayer.util.Assertions;
import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
-import java.util.Stack;
/**
- * An event-driven incremental EBML reader base class.
+ * Basic event-driven incremental EBML parser which needs an {@link EbmlEventHandler} to
+ * define IDs/types and react to events.
*
*
- *