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 queue); + void disable(List queue); /** * Indicates to the source that it should still be checking for updates to the stream. @@ -100,4 +100,13 @@ public interface ChunkSource { */ IOException getError(); + /** + * Invoked when the {@link ChunkSampleSource} encounters an error loading a chunk obtained from + * this source. + * + * @param chunk The chunk whose load encountered the error. + * @param e The error. + */ + void onChunkLoadError(Chunk chunk, Exception e); + } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java index d7d301404d..875956c0ee 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java @@ -15,12 +15,14 @@ */ package com.google.android.exoplayer.chunk; +import com.google.android.exoplayer.util.Assertions; + import java.util.Comparator; /** * A format definition for streams. */ -public final class Format { +public class Format { /** * Sorts {@link Format} objects in order of decreasing bandwidth. @@ -29,7 +31,7 @@ public final class Format { @Override public int compare(Format a, Format b) { - return b.bandwidth - a.bandwidth; + return b.bitrate - a.bitrate; } } @@ -37,7 +39,7 @@ public final class Format { /** * An identifier for the format. */ - public final int id; + public final String id; /** * The mime type of the format. @@ -65,8 +67,16 @@ public final class Format { public final int audioSamplingRate; /** - * The average bandwidth in bytes per second. + * The average bandwidth in bits per second. */ + public final int bitrate; + + /** + * The average bandwidth in bytes per second. + * + * @deprecated Use {@link #bitrate}. However note that the units of measurement are different. + */ + @Deprecated public final int bandwidth; /** @@ -76,17 +86,38 @@ public final class Format { * @param height The height of the video in pixels, or -1 for non-video formats. * @param numChannels The number of audio channels, or -1 for non-audio formats. * @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats. - * @param bandwidth The average bandwidth of the format in bytes per second. + * @param bitrate The average bandwidth of the format in bits per second. */ - public Format(int id, String mimeType, int width, int height, int numChannels, - int audioSamplingRate, int bandwidth) { - this.id = id; + public Format(String id, String mimeType, int width, int height, int numChannels, + int audioSamplingRate, int bitrate) { + this.id = Assertions.checkNotNull(id); this.mimeType = mimeType; this.width = width; this.height = height; this.numChannels = numChannels; this.audioSamplingRate = audioSamplingRate; - this.bandwidth = bandwidth; + this.bitrate = bitrate; + this.bandwidth = bitrate / 8; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + /** + * Implements equality based on {@link #id} only. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Format other = (Format) obj; + return other.id.equals(id); } } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java index 7998c5ebde..1a87b9a142 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java @@ -146,7 +146,7 @@ public interface FormatEvaluator { public void evaluate(List queue, long playbackPositionUs, Format[] formats, Evaluation evaluation) { Format newFormat = formats[random.nextInt(formats.length)]; - if (evaluation.format != null && evaluation.format.id != newFormat.id) { + if (evaluation.format != null && !evaluation.format.id.equals(newFormat.id)) { evaluation.trigger = TRIGGER_ADAPTIVE; } evaluation.format = newFormat; @@ -236,8 +236,8 @@ public interface FormatEvaluator { : queue.get(queue.size() - 1).endTimeUs - playbackPositionUs; Format current = evaluation.format; Format ideal = determineIdealFormat(formats, bandwidthMeter.getEstimate()); - boolean isHigher = ideal != null && current != null && ideal.bandwidth > current.bandwidth; - boolean isLower = ideal != null && current != null && ideal.bandwidth < current.bandwidth; + boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate; + boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate; if (isHigher) { if (bufferedDurationUs < minDurationForQualityIncreaseUs) { // The ideal format is a higher quality, but we have insufficient buffer to @@ -247,11 +247,11 @@ public interface FormatEvaluator { // We're switching from an SD stream to a stream of higher resolution. Consider // discarding already buffered media chunks. Specifically, discard media chunks starting // from the first one that is of lower bandwidth, lower resolution and that is not HD. - for (int i = 0; i < queue.size(); i++) { + for (int i = 1; i < queue.size(); i++) { MediaChunk thisChunk = queue.get(i); long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs; if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs - && thisChunk.format.bandwidth < ideal.bandwidth + && thisChunk.format.bitrate < ideal.bitrate && thisChunk.format.height < ideal.height && thisChunk.format.height < 720 && thisChunk.format.width < 1280) { @@ -280,7 +280,7 @@ public interface FormatEvaluator { long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate); for (int i = 0; i < formats.length; i++) { Format format = formats[i]; - if (format.bandwidth <= effectiveBandwidth) { + if ((format.bitrate / 8) <= effectiveBandwidth) { return format; } } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java index ad22645be6..51ebac65c2 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java @@ -73,9 +73,7 @@ public abstract class MediaChunk extends Chunk { /** * Seeks to the beginning of the chunk. */ - public final void seekToStart() { - seekTo(startTimeUs, false); - } + public abstract void seekToStart(); /** * Seeks to the specified position within the chunk. @@ -89,8 +87,22 @@ public abstract class MediaChunk extends Chunk { */ public abstract boolean seekTo(long positionUs, boolean allowNoop); + /** + * Prepares the chunk for reading. Does nothing if the chunk is already prepared. + *

+ * 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 psshInfo; + /** * @param dataSource A {@link DataSource} for loading the data. * @param dataSpec Defines the data to be loaded. * @param format The format of the stream to which this chunk belongs. - * @param extractor The extractor that will be used to extract the samples. * @param trigger The reason for this chunk being selected. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. - * @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor. * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. + * @param extractor The extractor that will be used to extract the samples. + * @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might + * contain a moov atom defining the media format of the chunk. This parameter can always be + * safely set to true. Setting to false where the chunk is known to not be self contained may + * improve startup latency. + * @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor. */ public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, - int trigger, FragmentedMp4Extractor extractor, long startTimeUs, long endTimeUs, - long sampleOffsetUs, int nextChunkIndex) { + int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, + FragmentedMp4Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) { super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex); this.extractor = extractor; + this.maybeSelfContained = maybeSelfContained; this.sampleOffsetUs = sampleOffsetUs; } + @Override + public void seekToStart() { + extractor.seekTo(0, false); + resetReadPosition(); + } + @Override public boolean seekTo(long positionUs, boolean allowNoop) { long seekTimeUs = positionUs + sampleOffsetUs; @@ -64,6 +80,29 @@ public final class Mp4MediaChunk extends MediaChunk { return isDiscontinuous; } + @Override + public boolean prepare() throws ParserException { + if (!prepared) { + if (maybeSelfContained) { + // Read up to the first sample. Once we're there, we know that the extractor must have + // parsed a moov atom if the chunk contains one. + NonBlockingInputStream inputStream = getNonBlockingInputStream(); + Assertions.checkState(inputStream != null); + int result = extractor.read(inputStream, null); + prepared = (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0; + } else { + // We know there isn't a moov atom. The extractor must have parsed one from a separate + // initialization chunk. + prepared = true; + } + if (prepared) { + mediaFormat = Assertions.checkNotNull(extractor.getFormat()); + psshInfo = extractor.getPsshInfo(); + } + } + return prepared; + } + @Override public boolean read(SampleHolder holder) throws ParserException { NonBlockingInputStream inputStream = getNonBlockingInputStream(); @@ -78,12 +117,12 @@ public final class Mp4MediaChunk extends MediaChunk { @Override public MediaFormat getMediaFormat() { - return extractor.getFormat(); + return mediaFormat; } @Override public Map getPsshInfo() { - return extractor.getPsshInfo(); + return psshInfo; } } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java index 98721cce21..2c7cf33649 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java @@ -68,7 +68,7 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent { } @Override - public void disable(List queue) { + public void disable(List queue) { selectedSource.disable(queue); enabled = false; } @@ -102,4 +102,9 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent { } } + @Override + public void onChunkLoadError(Chunk chunk, Exception e) { + selectedSource.onChunkLoadError(chunk, e); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java index 893e28b507..6fa2f08962 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java @@ -77,6 +77,11 @@ public class SingleSampleMediaChunk extends MediaChunk { this.headerData = headerData; } + @Override + public boolean prepare() { + return true; + } + @Override public boolean read(SampleHolder holder) { NonBlockingInputStream inputStream = getNonBlockingInputStream(); @@ -109,6 +114,11 @@ public class SingleSampleMediaChunk extends MediaChunk { return true; } + @Override + public void seekToStart() { + resetReadPosition(); + } + @Override public boolean seekTo(long positionUs, boolean allowNoop) { resetReadPosition(); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java index 1c86c23865..f7ca26244a 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java @@ -50,6 +50,11 @@ public final class WebmMediaChunk extends MediaChunk { this.extractor = extractor; } + @Override + public void seekToStart() { + seekTo(0, false); + } + @Override public boolean seekTo(long positionUs, boolean allowNoop) { boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop); @@ -59,6 +64,11 @@ public final class WebmMediaChunk extends MediaChunk { return isDiscontinuous; } + @Override + public boolean prepare() { + return true; + } + @Override public boolean read(SampleHolder holder) { NonBlockingInputStream inputStream = getNonBlockingInputStream(); diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java index 0bea6f09a8..e76152859f 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java @@ -27,18 +27,18 @@ import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.Mp4MediaChunk; +import com.google.android.exoplayer.dash.mpd.RangedUri; import com.google.android.exoplayer.dash.mpd.Representation; -import com.google.android.exoplayer.parser.SegmentIndex; import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.NonBlockingInputStream; -import android.util.Log; -import android.util.SparseArray; +import android.net.Uri; import java.io.IOException; import java.util.Arrays; +import java.util.HashMap; import java.util.List; /** @@ -46,26 +46,17 @@ import java.util.List; */ public class DashMp4ChunkSource implements ChunkSource { - public static final int DEFAULT_NUM_SEGMENTS_PER_CHUNK = 1; - - private static final int EXPECTED_INITIALIZATION_RESULT = - FragmentedMp4Extractor.RESULT_END_OF_STREAM - | FragmentedMp4Extractor.RESULT_READ_MOOV - | FragmentedMp4Extractor.RESULT_READ_SIDX; - - private static final String TAG = "DashMp4ChunkSource"; - private final TrackInfo trackInfo; private final DataSource dataSource; private final FormatEvaluator evaluator; private final Evaluation evaluation; private final int maxWidth; private final int maxHeight; - private final int numSegmentsPerChunk; private final Format[] formats; - private final SparseArray representations; - private final SparseArray extractors; + private final HashMap representations; + private final HashMap extractors; + private final HashMap segmentIndexes; private boolean lastChunkWasInitialization; @@ -76,26 +67,14 @@ public class DashMp4ChunkSource implements ChunkSource { */ public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator, Representation... representations) { - this(dataSource, evaluator, DEFAULT_NUM_SEGMENTS_PER_CHUNK, representations); - } - - /** - * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param evaluator Selects from the available formats. - * @param numSegmentsPerChunk The number of segments (as defined in the stream's segment index) - * that should be grouped into a single chunk. - * @param representations The representations to be considered by the source. - */ - public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator, - int numSegmentsPerChunk, Representation... representations) { this.dataSource = dataSource; this.evaluator = evaluator; - this.numSegmentsPerChunk = numSegmentsPerChunk; this.formats = new Format[representations.length]; - this.extractors = new SparseArray(); - this.representations = new SparseArray(); + this.extractors = new HashMap(); + this.segmentIndexes = new HashMap(); + this.representations = new HashMap(); this.trackInfo = new TrackInfo(representations[0].format.mimeType, - representations[0].periodDuration * 1000); + representations[0].periodDurationMs * 1000); this.evaluation = new Evaluation(); int maxWidth = 0; int maxHeight = 0; @@ -103,8 +82,12 @@ public class DashMp4ChunkSource implements ChunkSource { formats[i] = representations[i].format; maxWidth = Math.max(formats[i].width, maxWidth); maxHeight = Math.max(formats[i].height, maxHeight); - extractors.append(formats[i].id, new FragmentedMp4Extractor()); + extractors.put(formats[i].id, new FragmentedMp4Extractor()); this.representations.put(formats[i].id, representations[i]); + DashSegmentIndex segmentIndex = representations[i].getIndex(); + if (segmentIndex != null) { + segmentIndexes.put(formats[i].id, segmentIndex); + } } this.maxWidth = maxWidth; this.maxHeight = maxHeight; @@ -129,7 +112,7 @@ public class DashMp4ChunkSource implements ChunkSource { } @Override - public void disable(List queue) { + public void disable(List queue) { evaluator.disable(); } @@ -152,7 +135,7 @@ public class DashMp4ChunkSource implements ChunkSource { out.chunk = null; return; } else if (out.queueSize == queue.size() && out.chunk != null - && out.chunk.format.id == selectedFormat.id) { + && out.chunk.format.id.equals(selectedFormat.id)) { // We already have a chunk, and the evaluation hasn't changed either the format or the size // of the queue. Leave unchanged. return; @@ -160,29 +143,39 @@ public class DashMp4ChunkSource implements ChunkSource { Representation selectedRepresentation = representations.get(selectedFormat.id); FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id); + + RangedUri pendingInitializationUri = null; + RangedUri pendingIndexUri = null; if (extractor.getTrack() == null) { - Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor, - dataSource, evaluation.trigger); + pendingInitializationUri = selectedRepresentation.getInitializationUri(); + } + if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) { + pendingIndexUri = selectedRepresentation.getIndexUri(); + } + if (pendingInitializationUri != null || pendingIndexUri != null) { + // We have initialization and/or index requests to make. + Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri, + selectedRepresentation, extractor, dataSource, evaluation.trigger); lastChunkWasInitialization = true; out.chunk = initializationChunk; return; } - int nextIndex; + int nextSegmentNum; + DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id); if (queue.isEmpty()) { - nextIndex = Arrays.binarySearch(extractor.getSegmentIndex().timesUs, seekPositionUs); - nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex; + nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs); } else { - nextIndex = queue.get(out.queueSize - 1).nextChunkIndex; + nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex; } - if (nextIndex == -1) { + if (nextSegmentNum == -1) { out.chunk = null; return; } - Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource, - extractor.getSegmentIndex(), nextIndex, evaluation.trigger, numSegmentsPerChunk); + Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor, + dataSource, nextSegmentNum, evaluation.trigger); lastChunkWasInitialization = false; out.chunk = nextMediaChunk; } @@ -192,75 +185,80 @@ public class DashMp4ChunkSource implements ChunkSource { return null; } - private static Chunk newInitializationChunk(Representation representation, - FragmentedMp4Extractor extractor, DataSource dataSource, int trigger) { - DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1, - representation.getCacheKey()); - return new InitializationMp4Loadable(dataSource, dataSpec, trigger, extractor, representation); + @Override + public void onChunkLoadError(Chunk chunk, Exception e) { + // Do nothing. } - private static Chunk newMediaChunk(Representation representation, - FragmentedMp4Extractor extractor, DataSource dataSource, SegmentIndex sidx, int index, - int trigger, int numSegmentsPerChunk) { - - // Computes the segments to included in the next fetch. - int numSegmentsToFetch = Math.min(numSegmentsPerChunk, sidx.length - index); - int lastSegmentInChunk = index + numSegmentsToFetch - 1; - int nextIndex = lastSegmentInChunk == sidx.length - 1 ? -1 : lastSegmentInChunk + 1; - - long startTimeUs = sidx.timesUs[index]; - - // Compute the end time, prefer to use next segment start time if there is a next segment. - long endTimeUs = nextIndex == -1 ? - sidx.timesUs[lastSegmentInChunk] + sidx.durationsUs[lastSegmentInChunk] : - sidx.timesUs[nextIndex]; - - long offset = (int) representation.indexEnd + 1 + sidx.offsets[index]; - - // Compute combined segments byte length. - long size = 0; - for (int i = index; i <= lastSegmentInChunk; i++) { - size += sidx.sizes[i]; + private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri, + Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource, + int trigger) { + int expectedExtractorResult = FragmentedMp4Extractor.RESULT_END_OF_STREAM; + long indexAnchor = 0; + RangedUri requestUri; + if (initializationUri != null) { + // It's common for initialization and index data to be stored adjacently. Attempt to merge + // the two requests together to request both at once. + expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_MOOV; + requestUri = initializationUri.attemptMerge(indexUri); + if (requestUri != null) { + expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX; + indexAnchor = indexUri.start + indexUri.length; + } else { + requestUri = initializationUri; + } + } else { + requestUri = indexUri; + indexAnchor = indexUri.start + indexUri.length; + expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX; } - - DataSpec dataSpec = new DataSpec(representation.uri, offset, size, + DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, representation.getCacheKey()); - return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, extractor, - startTimeUs, endTimeUs, 0, nextIndex); + return new InitializationMp4Loadable(dataSource, dataSpec, trigger, representation.format, + extractor, expectedExtractorResult, indexAnchor); } - private static class InitializationMp4Loadable extends Chunk { + private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex, + FragmentedMp4Extractor extractor, DataSource dataSource, int segmentNum, int trigger) { + int lastSegmentNum = segmentIndex.getLastSegmentNum(); + int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1; + long startTimeUs = segmentIndex.getTimeUs(segmentNum); + long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1) + : startTimeUs + segmentIndex.getDurationUs(segmentNum); + RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum); + DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, + representation.getCacheKey()); + return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs, + endTimeUs, nextSegmentNum, extractor, false, 0); + } + + private class InitializationMp4Loadable extends Chunk { - private final Representation representation; private final FragmentedMp4Extractor extractor; + private final int expectedExtractorResult; + private final long indexAnchor; + private final Uri uri; public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger, - FragmentedMp4Extractor extractor, Representation representation) { - super(dataSource, dataSpec, representation.format, trigger); + Format format, FragmentedMp4Extractor extractor, int expectedExtractorResult, + long indexAnchor) { + super(dataSource, dataSpec, format, trigger); this.extractor = extractor; - this.representation = representation; + this.expectedExtractorResult = expectedExtractorResult; + this.indexAnchor = indexAnchor; + this.uri = dataSpec.uri; } @Override protected void consumeStream(NonBlockingInputStream stream) throws IOException { int result = extractor.read(stream, null); - if (result != EXPECTED_INITIALIZATION_RESULT) { - throw new ParserException("Invalid initialization data"); + if (result != expectedExtractorResult) { + throw new ParserException("Invalid extractor result. Expected " + + expectedExtractorResult + ", got " + result); } - validateSegmentIndex(extractor.getSegmentIndex()); - } - - private void validateSegmentIndex(SegmentIndex segmentIndex) { - long expectedIndexLen = representation.indexEnd - representation.indexStart + 1; - if (segmentIndex.sizeBytes != expectedIndexLen) { - Log.w(TAG, "Sidx length mismatch: sidxLen = " + segmentIndex.sizeBytes + - ", ExpectedLen = " + expectedIndexLen); - } - long sidxContentLength = segmentIndex.offsets[segmentIndex.length - 1] + - segmentIndex.sizes[segmentIndex.length - 1] + representation.indexEnd + 1; - if (sidxContentLength != representation.contentLength) { - Log.w(TAG, "ContentLength mismatch: Actual = " + sidxContentLength + - ", Expected = " + representation.contentLength); + if ((result & FragmentedMp4Extractor.RESULT_READ_SIDX) != 0) { + segmentIndexes.put(format.id, + new DashWrappingSegmentIndex(extractor.getSegmentIndex(), uri, indexAnchor)); } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java index 7f518723e9..133b4879ac 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java @@ -27,18 +27,19 @@ import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.WebmMediaChunk; +import com.google.android.exoplayer.dash.mpd.RangedUri; import com.google.android.exoplayer.dash.mpd.Representation; -import com.google.android.exoplayer.parser.SegmentIndex; +import com.google.android.exoplayer.parser.webm.DefaultWebmExtractor; import com.google.android.exoplayer.parser.webm.WebmExtractor; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.NonBlockingInputStream; -import android.util.Log; -import android.util.SparseArray; +import android.net.Uri; import java.io.IOException; import java.util.Arrays; +import java.util.HashMap; import java.util.List; /** @@ -46,37 +47,30 @@ import java.util.List; */ public class DashWebmChunkSource implements ChunkSource { - private static final String TAG = "DashWebmChunkSource"; - private final TrackInfo trackInfo; private final DataSource dataSource; private final FormatEvaluator evaluator; private final Evaluation evaluation; private final int maxWidth; private final int maxHeight; - private final int numSegmentsPerChunk; private final Format[] formats; - private final SparseArray representations; - private final SparseArray extractors; + private final HashMap representations; + private final HashMap extractors; + private final HashMap segmentIndexes; private boolean lastChunkWasInitialization; - public DashWebmChunkSource( - DataSource dataSource, FormatEvaluator evaluator, Representation... representations) { - this(dataSource, evaluator, 1, representations); - } - public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator, - int numSegmentsPerChunk, Representation... representations) { + Representation... representations) { this.dataSource = dataSource; this.evaluator = evaluator; - this.numSegmentsPerChunk = numSegmentsPerChunk; this.formats = new Format[representations.length]; - this.extractors = new SparseArray(); - this.representations = new SparseArray(); + this.extractors = new HashMap(); + this.segmentIndexes = new HashMap(); + this.representations = new HashMap(); this.trackInfo = new TrackInfo( - representations[0].format.mimeType, representations[0].periodDuration * 1000); + representations[0].format.mimeType, representations[0].periodDurationMs * 1000); this.evaluation = new Evaluation(); int maxWidth = 0; int maxHeight = 0; @@ -84,8 +78,12 @@ public class DashWebmChunkSource implements ChunkSource { formats[i] = representations[i].format; maxWidth = Math.max(formats[i].width, maxWidth); maxHeight = Math.max(formats[i].height, maxHeight); - extractors.append(formats[i].id, new WebmExtractor()); + extractors.put(formats[i].id, new DefaultWebmExtractor()); this.representations.put(formats[i].id, representations[i]); + DashSegmentIndex segmentIndex = representations[i].getIndex(); + if (segmentIndex != null) { + segmentIndexes.put(formats[i].id, segmentIndex); + } } this.maxWidth = maxWidth; this.maxHeight = maxHeight; @@ -110,7 +108,7 @@ public class DashWebmChunkSource implements ChunkSource { } @Override - public void disable(List queue) { + public void disable(List queue) { evaluator.disable(); } @@ -133,7 +131,7 @@ public class DashWebmChunkSource implements ChunkSource { out.chunk = null; return; } else if (out.queueSize == queue.size() && out.chunk != null - && out.chunk.format.id == selectedFormat.id) { + && out.chunk.format.id.equals(selectedFormat.id)) { // We already have a chunk, and the evaluation hasn't changed either the format or the size // of the queue. Leave unchanged. return; @@ -141,29 +139,34 @@ public class DashWebmChunkSource implements ChunkSource { Representation selectedRepresentation = representations.get(selectedFormat.id); WebmExtractor extractor = extractors.get(selectedRepresentation.format.id); + if (!extractor.isPrepared()) { - Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor, - dataSource, evaluation.trigger); + // TODO: This code forces cues to exist and to immediately follow the initialization + // data. Webm extractor should be generalized to allow cues to be optional. See [redacted]. + RangedUri initializationUri = selectedRepresentation.getInitializationUri().attemptMerge( + selectedRepresentation.getIndexUri()); + Chunk initializationChunk = newInitializationChunk(initializationUri, selectedRepresentation, + extractor, dataSource, evaluation.trigger); lastChunkWasInitialization = true; out.chunk = initializationChunk; return; } - int nextIndex; + int nextSegmentNum; + DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id); if (queue.isEmpty()) { - nextIndex = Arrays.binarySearch(extractor.getCues().timesUs, seekPositionUs); - nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex; + nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs); } else { - nextIndex = queue.get(out.queueSize - 1).nextChunkIndex; + nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex; } - if (nextIndex == -1) { + if (nextSegmentNum == -1) { out.chunk = null; return; } - Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource, - extractor.getCues(), nextIndex, evaluation.trigger, numSegmentsPerChunk); + Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor, + dataSource, nextSegmentNum, evaluation.trigger); lastChunkWasInitialization = false; out.chunk = nextMediaChunk; } @@ -173,53 +176,43 @@ public class DashWebmChunkSource implements ChunkSource { return null; } - private static Chunk newInitializationChunk(Representation representation, - WebmExtractor extractor, DataSource dataSource, int trigger) { - DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1, - representation.getCacheKey()); - return new InitializationWebmLoadable(dataSource, dataSpec, trigger, extractor, representation); + @Override + public void onChunkLoadError(Chunk chunk, Exception e) { + // Do nothing. } - private static Chunk newMediaChunk(Representation representation, - WebmExtractor extractor, DataSource dataSource, SegmentIndex cues, int index, - int trigger, int numSegmentsPerChunk) { + private Chunk newInitializationChunk(RangedUri initializationUri, Representation representation, + WebmExtractor extractor, DataSource dataSource, int trigger) { + DataSpec dataSpec = new DataSpec(initializationUri.getUri(), initializationUri.start, + initializationUri.length, representation.getCacheKey()); + return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format, + extractor); + } - // Computes the segments to included in the next fetch. - int numSegmentsToFetch = Math.min(numSegmentsPerChunk, cues.length - index); - int lastSegmentInChunk = index + numSegmentsToFetch - 1; - int nextIndex = lastSegmentInChunk == cues.length - 1 ? -1 : lastSegmentInChunk + 1; - - long startTimeUs = cues.timesUs[index]; - - // Compute the end time, prefer to use next segment start time if there is a next segment. - long endTimeUs = nextIndex == -1 ? - cues.timesUs[lastSegmentInChunk] + cues.durationsUs[lastSegmentInChunk] : - cues.timesUs[nextIndex]; - - long offset = cues.offsets[index]; - - // Compute combined segments byte length. - long size = 0; - for (int i = index; i <= lastSegmentInChunk; i++) { - size += cues.sizes[i]; - } - - DataSpec dataSpec = new DataSpec(representation.uri, offset, size, + private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex, + WebmExtractor extractor, DataSource dataSource, int segmentNum, int trigger) { + int lastSegmentNum = segmentIndex.getLastSegmentNum(); + int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1; + long startTimeUs = segmentIndex.getTimeUs(segmentNum); + long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1) + : startTimeUs + segmentIndex.getDurationUs(segmentNum); + RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum); + DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, representation.getCacheKey()); return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor, - startTimeUs, endTimeUs, nextIndex); + startTimeUs, endTimeUs, nextSegmentNum); } - private static class InitializationWebmLoadable extends Chunk { + private class InitializationWebmLoadable extends Chunk { - private final Representation representation; private final WebmExtractor extractor; + private final Uri uri; public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger, - WebmExtractor extractor, Representation representation) { - super(dataSource, dataSpec, representation.format, trigger); + Format format, WebmExtractor extractor) { + super(dataSource, dataSpec, format, trigger); this.extractor = extractor; - this.representation = representation; + this.uri = dataSpec.uri; } @Override @@ -228,22 +221,7 @@ public class DashWebmChunkSource implements ChunkSource { if (!extractor.isPrepared()) { throw new ParserException("Invalid initialization data"); } - validateCues(extractor.getCues()); - } - - private void validateCues(SegmentIndex cues) { - long expectedSizeBytes = representation.indexEnd - representation.indexStart + 1; - if (cues.sizeBytes != expectedSizeBytes) { - Log.w(TAG, "Cues length mismatch: got " + cues.sizeBytes + - " but expected " + expectedSizeBytes); - } - long expectedContentLength = cues.offsets[cues.length - 1] + - cues.sizes[cues.length - 1] + representation.indexEnd + 1; - if (representation.contentLength > 0 - && expectedContentLength != representation.contentLength) { - Log.w(TAG, "ContentLength mismatch: got " + expectedContentLength + - " but expected " + representation.contentLength); - } + segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getCues(), uri, 0)); } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java index 82e91c52df..da294190f7 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer.dash.mpd; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.util.ManifestFetcher; +import android.net.Uri; + import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; @@ -57,9 +59,9 @@ public final class MediaPresentationDescriptionFetcher extends @Override protected MediaPresentationDescription parse(InputStream stream, String inputEncoding, - String contentId) throws IOException, ParserException { + String contentId, Uri baseUrl) throws IOException, ParserException { try { - return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId); + return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl); } catch (XmlPullParserException e) { throw new ParserException(e); } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 9b0df77761..3bf9666006 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -17,11 +17,15 @@ package com.google.android.exoplayer.dash.mpd; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.chunk.Format; -import com.google.android.exoplayer.upstream.DataSpec; +import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentList; +import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTemplate; +import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTimelineElement; +import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase; +import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; import android.net.Uri; -import android.util.Log; +import android.text.TextUtils; import org.xml.sax.helpers.DefaultHandler; import org.xmlpull.v1.XmlPullParser; @@ -38,15 +42,8 @@ import java.util.regex.Pattern; /** * A parser of media presentation description files. */ -/* - * TODO: Parse representation base attributes at multiple levels, and normalize the resulting - * datastructure. - * TODO: Decide how best to represent missing integer/double/long attributes. - */ public class MediaPresentationDescriptionParser extends DefaultHandler { - private static final String TAG = "MediaPresentationDescriptionParser"; - // Note: Does not support the date part of ISO 8601 private static final Pattern DURATION = Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$"); @@ -61,20 +58,23 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { } } + // MPD parsing. + /** * Parses a manifest from the provided {@link InputStream}. * * @param inputStream The stream from which to parse the manifest. * @param inputEncoding The encoding of the input. * @param contentId The content id of the media. + * @param baseUrl The url that any relative urls defined within the manifest are relative to. * @return The parsed manifest. * @throws IOException If a problem occurred reading from the stream. * @throws XmlPullParserException If a problem occurred parsing the stream as xml. * @throws ParserException If a problem occurred parsing the xml as a DASH mpd. */ public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream, - String inputEncoding, String contentId) throws XmlPullParserException, IOException, - ParserException { + String inputEncoding, String contentId, Uri baseUrl) throws XmlPullParserException, + IOException, ParserException { XmlPullParser xpp = xmlParserFactory.newPullParser(); xpp.setInput(inputStream, inputEncoding); int eventType = xpp.next(); @@ -82,123 +82,139 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { throw new ParserException( "inputStream does not contain a valid media presentation description"); } - return parseMediaPresentationDescription(xpp, contentId); + return parseMediaPresentationDescription(xpp, contentId, baseUrl); } private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp, - String contentId) throws XmlPullParserException, IOException { - long duration = parseDurationMs(xpp, "mediaPresentationDuration"); - long minBufferTime = parseDurationMs(xpp, "minBufferTime"); + String contentId, Uri baseUrl) throws XmlPullParserException, IOException { + long durationMs = parseDurationMs(xpp, "mediaPresentationDuration"); + long minBufferTimeMs = parseDurationMs(xpp, "minBufferTime"); String typeString = xpp.getAttributeValue(null, "type"); boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false; - long minUpdateTime = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1; + long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1; List periods = new ArrayList(); do { xpp.next(); - if (isStartTag(xpp, "Period")) { - periods.add(parsePeriod(xpp, contentId, duration)); + if (isStartTag(xpp, "BaseURL")) { + baseUrl = parseBaseUrl(xpp, baseUrl); + } else if (isStartTag(xpp, "Period")) { + periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs)); } } while (!isEndTag(xpp, "MPD")); - return new MediaPresentationDescription(duration, minBufferTime, dynamic, minUpdateTime, + return new MediaPresentationDescription(durationMs, minBufferTimeMs, dynamic, minUpdateTimeMs, periods); } - private Period parsePeriod(XmlPullParser xpp, String contentId, long mediaPresentationDuration) + private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs) throws XmlPullParserException, IOException { - int id = parseInt(xpp, "id"); - long start = parseDurationMs(xpp, "start", 0); - long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration); - + String id = xpp.getAttributeValue(null, "id"); + long startMs = parseDurationMs(xpp, "start", 0); + long durationMs = parseDurationMs(xpp, "duration", mpdDurationMs); + SegmentBase segmentBase = null; List adaptationSets = new ArrayList(); - List segmentTimelineList = null; - int segmentStartNumber = 0; - int segmentTimescale = 0; - long presentationTimeOffset = 0; do { xpp.next(); - if (isStartTag(xpp, "AdaptationSet")) { - adaptationSets.add(parseAdaptationSet(xpp, contentId, start, duration, - segmentTimelineList)); + if (isStartTag(xpp, "BaseURL")) { + baseUrl = parseBaseUrl(xpp, baseUrl); + } else if (isStartTag(xpp, "AdaptationSet")) { + adaptationSets.add(parseAdaptationSet(xpp, contentId, baseUrl, startMs, durationMs, + segmentBase)); + } else if (isStartTag(xpp, "SegmentBase")) { + segmentBase = parseSegmentBase(xpp, baseUrl, null); } else if (isStartTag(xpp, "SegmentList")) { - segmentStartNumber = parseInt(xpp, "startNumber"); - segmentTimescale = parseInt(xpp, "timescale"); - presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", 0); - segmentTimelineList = parsePeriodSegmentList(xpp, segmentStartNumber); + segmentBase = parseSegmentList(xpp, baseUrl, null, durationMs); + } else if (isStartTag(xpp, "SegmentTemplate")) { + segmentBase = parseSegmentTemplate(xpp, baseUrl, null, durationMs); } } while (!isEndTag(xpp, "Period")); - return new Period(id, start, duration, adaptationSets, segmentTimelineList, - segmentStartNumber, segmentTimescale, presentationTimeOffset); + return new Period(id, startMs, durationMs, adaptationSets); } - private List parsePeriodSegmentList( - XmlPullParser xpp, long segmentStartNumber) throws XmlPullParserException, IOException { - List segmentTimelineList = new ArrayList(); + // AdaptationSet parsing. - do { - xpp.next(); - if (isStartTag(xpp, "SegmentTimeline")) { - do { - xpp.next(); - if (isStartTag(xpp, "S")) { - long duration = parseLong(xpp, "d"); - segmentTimelineList.add(new Segment.Timeline(segmentStartNumber, duration)); - segmentStartNumber++; - } - } while (!isEndTag(xpp, "SegmentTimeline")); - } - } while (!isEndTag(xpp, "SegmentList")); - - return segmentTimelineList; - } - - private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, long periodStart, - long periodDuration, List segmentTimelineList) + private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri baseUrl, + long periodStartMs, long periodDurationMs, SegmentBase segmentBase) throws XmlPullParserException, IOException { - int id = -1; - int contentType = AdaptationSet.TYPE_UNKNOWN; - // TODO: Correctly handle other common attributes and elements. See 23009-1 Table 9. String mimeType = xpp.getAttributeValue(null, "mimeType"); - if (mimeType != null) { - if (MimeTypes.isAudio(mimeType)) { - contentType = AdaptationSet.TYPE_AUDIO; - } else if (MimeTypes.isVideo(mimeType)) { - contentType = AdaptationSet.TYPE_VIDEO; - } else if (MimeTypes.isText(mimeType) - || mimeType.equalsIgnoreCase(MimeTypes.APPLICATION_TTML)) { - contentType = AdaptationSet.TYPE_TEXT; - } - } + int contentType = parseAdaptationSetTypeFromMimeType(mimeType); + int id = -1; List contentProtections = null; List representations = new ArrayList(); do { xpp.next(); - if (contentType != AdaptationSet.TYPE_UNKNOWN) { - if (isStartTag(xpp, "ContentProtection")) { - if (contentProtections == null) { - contentProtections = new ArrayList(); - } - contentProtections.add(parseContentProtection(xpp)); - } else if (isStartTag(xpp, "ContentComponent")) { - id = Integer.parseInt(xpp.getAttributeValue(null, "id")); - String contentTypeString = xpp.getAttributeValue(null, "contentType"); - contentType = "video".equals(contentTypeString) ? AdaptationSet.TYPE_VIDEO - : "audio".equals(contentTypeString) ? AdaptationSet.TYPE_AUDIO - : AdaptationSet.TYPE_UNKNOWN; - } else if (isStartTag(xpp, "Representation")) { - representations.add(parseRepresentation(xpp, contentId, periodStart, periodDuration, - mimeType, segmentTimelineList)); + if (isStartTag(xpp, "BaseURL")) { + baseUrl = parseBaseUrl(xpp, baseUrl); + } else if (isStartTag(xpp, "ContentProtection")) { + if (contentProtections == null) { + contentProtections = new ArrayList(); } + contentProtections.add(parseContentProtection(xpp)); + } else if (isStartTag(xpp, "ContentComponent")) { + id = Integer.parseInt(xpp.getAttributeValue(null, "id")); + contentType = checkAdaptationSetTypeConsistency(contentType, + parseAdaptationSetType(xpp.getAttributeValue(null, "contentType"))); + } else if (isStartTag(xpp, "Representation")) { + Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs, + periodDurationMs, mimeType, segmentBase); + contentType = checkAdaptationSetTypeConsistency(contentType, + parseAdaptationSetTypeFromMimeType(representation.format.mimeType)); + representations.add(representation); + } else if (isStartTag(xpp, "SegmentBase")) { + segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase); + } else if (isStartTag(xpp, "SegmentList")) { + segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs); + } else if (isStartTag(xpp, "SegmentTemplate")) { + segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase, + periodDurationMs); } } while (!isEndTag(xpp, "AdaptationSet")); return new AdaptationSet(id, contentType, representations, contentProtections); } + private int parseAdaptationSetType(String contentType) { + return TextUtils.isEmpty(contentType) ? AdaptationSet.TYPE_UNKNOWN + : MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? AdaptationSet.TYPE_AUDIO + : MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? AdaptationSet.TYPE_VIDEO + : MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? AdaptationSet.TYPE_TEXT + : AdaptationSet.TYPE_UNKNOWN; + } + + private int parseAdaptationSetTypeFromMimeType(String mimeType) { + return TextUtils.isEmpty(mimeType) ? AdaptationSet.TYPE_UNKNOWN + : MimeTypes.isAudio(mimeType) ? AdaptationSet.TYPE_AUDIO + : MimeTypes.isVideo(mimeType) ? AdaptationSet.TYPE_VIDEO + : MimeTypes.isText(mimeType) || MimeTypes.isTtml(mimeType) ? AdaptationSet.TYPE_TEXT + : AdaptationSet.TYPE_UNKNOWN; + } + + /** + * Checks two adaptation set types for consistency, returning the consistent type, or throwing an + * {@link IllegalStateException} if the types are inconsistent. + *

+ * 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 segmentTimelineList) + // Representation parsing. + + private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl, + long periodStartMs, long periodDurationMs, String mimeType, SegmentBase segmentBase) throws XmlPullParserException, IOException { - int id; - try { - id = parseInt(xpp, "id"); - } catch (NumberFormatException nfe) { - Log.d(TAG, "Unable to parse id; " + nfe.getMessage()); - // TODO: need a way to generate a unique and stable id; use hashCode for now - id = xpp.getAttributeValue(null, "id").hashCode(); - } - int bandwidth = parseInt(xpp, "bandwidth") / 8; + String id = xpp.getAttributeValue(null, "id"); + int bandwidth = parseInt(xpp, "bandwidth"); int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); int width = parseInt(xpp, "width"); int height = parseInt(xpp, "height"); + mimeType = parseString(xpp, "mimeType", mimeType); - String mimeType = xpp.getAttributeValue(null, "mimeType"); - if (mimeType == null) { - mimeType = parentMimeType; - } - - String representationUrl = null; - long indexStart = -1; - long indexEnd = -1; - long initializationStart = -1; - long initializationEnd = -1; int numChannels = -1; - List segmentList = null; do { xpp.next(); if (isStartTag(xpp, "BaseURL")) { - xpp.next(); - representationUrl = xpp.getText(); + baseUrl = parseBaseUrl(xpp, baseUrl); } else if (isStartTag(xpp, "AudioChannelConfiguration")) { numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value")); } else if (isStartTag(xpp, "SegmentBase")) { - String[] indexRange = xpp.getAttributeValue(null, "indexRange").split("-"); - indexStart = Long.parseLong(indexRange[0]); - indexEnd = Long.parseLong(indexRange[1]); + segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase); } else if (isStartTag(xpp, "SegmentList")) { - segmentList = parseRepresentationSegmentList(xpp, segmentTimelineList); - } else if (isStartTag(xpp, "Initialization")) { - String[] indexRange = xpp.getAttributeValue(null, "range").split("-"); - initializationStart = Long.parseLong(indexRange[0]); - initializationEnd = Long.parseLong(indexRange[1]); + segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs); + } else if (isStartTag(xpp, "SegmentTemplate")) { + segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase, + periodDurationMs); } } while (!isEndTag(xpp, "Representation")); - Uri uri = Uri.parse(representationUrl); Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate, bandwidth); - if (segmentList == null) { - return new Representation(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED, - initializationStart, initializationEnd, indexStart, indexEnd, periodStart, - periodDuration); - } else { - return new SegmentedRepresentation(contentId, format, uri, initializationStart, - initializationEnd, indexStart, indexEnd, periodStart, periodDuration, segmentList); - } + return Representation.newInstance(periodStartMs, periodDurationMs, contentId, -1, format, + segmentBase); } - private List parseRepresentationSegmentList(XmlPullParser xpp, - List segmentTimelineList) throws XmlPullParserException, IOException { - List segmentList = new ArrayList(); - int i = 0; + // SegmentBase, SegmentList and SegmentTemplate parsing. + + private SingleSegmentBase parseSegmentBase(XmlPullParser xpp, Uri baseUrl, + SingleSegmentBase parent) throws XmlPullParserException, IOException { + + long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); + long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", + parent != null ? parent.presentationTimeOffset : 0); + + long indexStart = parent != null ? parent.indexStart : 0; + long indexLength = parent != null ? parent.indexLength : -1; + String indexRangeText = xpp.getAttributeValue(null, "indexRange"); + if (indexRangeText != null) { + String[] indexRange = indexRangeText.split("-"); + indexStart = Long.parseLong(indexRange[0]); + indexLength = Long.parseLong(indexRange[1]) - indexStart + 1; + } + + RangedUri initialization = parent != null ? parent.initialization : null; + do { + xpp.next(); + if (isStartTag(xpp, "Initialization")) { + initialization = parseInitialization(xpp, baseUrl); + } + } while (!isEndTag(xpp, "SegmentBase")); + + return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl, + indexStart, indexLength); + } + + private SegmentList parseSegmentList(XmlPullParser xpp, Uri baseUrl, SegmentList parent, + long periodDuration) throws XmlPullParserException, IOException { + + long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); + long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", + parent != null ? parent.presentationTimeOffset : 0); + long duration = parseLong(xpp, "duration", parent != null ? parent.duration : -1); + int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 0); + + RangedUri initialization = null; + List timeline = null; + List segments = null; do { xpp.next(); if (isStartTag(xpp, "Initialization")) { - String url = xpp.getAttributeValue(null, "sourceURL"); - String[] indexRange = xpp.getAttributeValue(null, "range").split("-"); - long initializationStart = Long.parseLong(indexRange[0]); - long initializationEnd = Long.parseLong(indexRange[1]); - segmentList.add(new Segment.Initialization(url, initializationStart, initializationEnd)); + initialization = parseInitialization(xpp, baseUrl); + } else if (isStartTag(xpp, "SegmentTimeline")) { + timeline = parseSegmentTimeline(xpp); } else if (isStartTag(xpp, "SegmentURL")) { - String url = xpp.getAttributeValue(null, "media"); - String mediaRange = xpp.getAttributeValue(null, "mediaRange"); - long sequenceNumber = segmentTimelineList.get(i).sequenceNumber; - long duration = segmentTimelineList.get(i).duration; - i++; - if (mediaRange != null) { - String[] mediaRangeArray = xpp.getAttributeValue(null, "mediaRange").split("-"); - long mediaStart = Long.parseLong(mediaRangeArray[0]); - segmentList.add(new Segment.Media(url, mediaStart, sequenceNumber, duration)); - } else { - segmentList.add(new Segment.Media(url, sequenceNumber, duration)); + if (segments == null) { + segments = new ArrayList(); } + segments.add(parseSegmentUrl(xpp, baseUrl)); } } while (!isEndTag(xpp, "SegmentList")); - return segmentList; + if (parent != null) { + initialization = initialization != null ? initialization : parent.initialization; + timeline = timeline != null ? timeline : parent.segmentTimeline; + segments = segments != null ? segments : parent.mediaSegments; + } + + return new SegmentList(initialization, timescale, presentationTimeOffset, periodDuration, + startNumber, duration, timeline, segments); } + private SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, Uri baseUrl, + SegmentTemplate parent, long periodDuration) throws XmlPullParserException, IOException { + + long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); + long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", + parent != null ? parent.presentationTimeOffset : 0); + long duration = parseLong(xpp, "duration", parent != null ? parent.duration : -1); + int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 0); + UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media", + parent != null ? parent.mediaTemplate : null); + UrlTemplate initializationTemplate = parseUrlTemplate(xpp, "initialization", + parent != null ? parent.initializationTemplate : null); + + RangedUri initialization = null; + List timeline = null; + + do { + xpp.next(); + if (isStartTag(xpp, "Initialization")) { + initialization = parseInitialization(xpp, baseUrl); + } else if (isStartTag(xpp, "SegmentTimeline")) { + timeline = parseSegmentTimeline(xpp); + } + } while (!isEndTag(xpp, "SegmentTemplate")); + + if (parent != null) { + initialization = initialization != null ? initialization : parent.initialization; + timeline = timeline != null ? timeline : parent.segmentTimeline; + } + + return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDuration, + startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); + } + + private List parseSegmentTimeline(XmlPullParser xpp) + throws XmlPullParserException, IOException { + List segmentTimeline = new ArrayList(); + long elapsedTime = 0; + do { + xpp.next(); + if (isStartTag(xpp, "S")) { + elapsedTime = parseLong(xpp, "t", elapsedTime); + long duration = parseLong(xpp, "d"); + int count = 1 + parseInt(xpp, "r", 0); + for (int i = 0; i < count; i++) { + segmentTimeline.add(new SegmentTimelineElement(elapsedTime, duration)); + elapsedTime += duration; + } + } + } while (!isEndTag(xpp, "SegmentTimeline")); + return segmentTimeline; + } + + private UrlTemplate parseUrlTemplate(XmlPullParser xpp, String name, + UrlTemplate defaultValue) { + String valueString = xpp.getAttributeValue(null, name); + if (valueString != null) { + return UrlTemplate.compile(valueString); + } + return defaultValue; + } + + private RangedUri parseInitialization(XmlPullParser xpp, Uri baseUrl) { + return parseRangedUrl(xpp, baseUrl, "sourceURL", "range"); + } + + private RangedUri parseSegmentUrl(XmlPullParser xpp, Uri baseUrl) { + return parseRangedUrl(xpp, baseUrl, "media", "mediaRange"); + } + + private RangedUri parseRangedUrl(XmlPullParser xpp, Uri baseUrl, String urlAttribute, + String rangeAttribute) { + String urlText = xpp.getAttributeValue(null, urlAttribute); + long rangeStart = 0; + long rangeLength = -1; + String rangeText = xpp.getAttributeValue(null, rangeAttribute); + if (rangeText != null) { + String[] rangeTextArray = rangeText.split("-"); + rangeStart = Long.parseLong(rangeTextArray[0]); + rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1; + } + return new RangedUri(baseUrl, urlText, rangeStart, rangeLength); + } + + // Utility methods. + protected static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException { return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName()); } @@ -313,25 +424,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName()); } - protected static int parseInt(XmlPullParser xpp, String name) { - String value = xpp.getAttributeValue(null, name); - return value == null ? -1 : Integer.parseInt(value); - } - - protected static long parseLong(XmlPullParser xpp, String name) { - return parseLong(xpp, name, -1); - } - - protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) { - String value = xpp.getAttributeValue(null, name); - return value == null ? defaultValue : Long.parseLong(value); - } - - private long parseDurationMs(XmlPullParser xpp, String name) { + private static long parseDurationMs(XmlPullParser xpp, String name) { return parseDurationMs(xpp, name, -1); } - private long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) { + private static long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) { String value = xpp.getAttributeValue(null, name); if (value != null) { Matcher matcher = DURATION.matcher(value); @@ -350,4 +447,38 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { return defaultValue; } + protected static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl) + throws XmlPullParserException, IOException { + xpp.next(); + String newBaseUrlText = xpp.getText(); + Uri newBaseUri = Uri.parse(newBaseUrlText); + if (!newBaseUri.isAbsolute()) { + newBaseUri = Uri.withAppendedPath(parentBaseUrl, newBaseUrlText); + } + return newBaseUri; + } + + protected static int parseInt(XmlPullParser xpp, String name) { + return parseInt(xpp, name, -1); + } + + protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) { + String value = xpp.getAttributeValue(null, name); + return value == null ? defaultValue : Integer.parseInt(value); + } + + protected static long parseLong(XmlPullParser xpp, String name) { + return parseLong(xpp, name, -1); + } + + protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) { + String value = xpp.getAttributeValue(null, name); + return value == null ? defaultValue : Long.parseLong(value); + } + + protected static String parseString(XmlPullParser xpp, String name, String defaultValue) { + String value = xpp.getAttributeValue(null, name); + return value == null ? defaultValue : value; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java index 4b33161acb..6fd3a71f4f 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java @@ -23,46 +23,37 @@ import java.util.List; */ public final class Period { - public final int id; + /** + * The period identifier, if one exists. + */ + public final String id; - public final long start; + /** + * The start time of the period in milliseconds. + */ + public final long startMs; - public final long duration; + /** + * The duration of the period in milliseconds, or -1 if the duration is unknown. + */ + public final long durationMs; + /** + * The adaptation sets belonging to the period. + */ public final List adaptationSets; - public final List segmentList; - - public final int segmentStartNumber; - - public final int segmentTimescale; - - public final long presentationTimeOffset; - - public Period(int id, long start, long duration, List adaptationSets) { - this(id, start, duration, adaptationSets, null, 0, 0, 0); - } - - public Period(int id, long start, long duration, List adaptationSets, - List segmentList, int segmentStartNumber, int segmentTimescale) { - this(id, start, duration, adaptationSets, segmentList, segmentStartNumber, segmentTimescale, 0); - } - - public Period(int id, long start, long duration, List adaptationSets, - List segmentList, int segmentStartNumber, int segmentTimescale, - long presentationTimeOffset) { + /** + * @param id The period identifier. May be null. + * @param start The start time of the period in milliseconds. + * @param duration The duration of the period in milliseconds, or -1 if the duration is unknown. + * @param adaptationSets The adaptation sets belonging to the period. + */ + public Period(String id, long start, long duration, List adaptationSets) { this.id = id; - this.start = start; - this.duration = duration; + this.startMs = start; + this.durationMs = duration; this.adaptationSets = Collections.unmodifiableList(adaptationSets); - if (segmentList != null) { - this.segmentList = Collections.unmodifiableList(segmentList); - } else { - this.segmentList = null; - } - this.segmentStartNumber = segmentStartNumber; - this.segmentTimescale = segmentTimescale; - this.presentationTimeOffset = presentationTimeOffset; } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java new file mode 100644 index 0000000000..cd18f85599 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java @@ -0,0 +1,138 @@ +/* + * 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.util.Assertions; + +import android.net.Uri; + +/** + * Defines a range of data located at a {@link Uri}. + */ +public final class RangedUri { + + /** + * The (zero based) index of the first byte of the range. + */ + public final long start; + + /** + * The length of the range, or -1 to indicate that the range is unbounded. + */ + public final long length; + + // The {@link Uri} is stored internally in two parts, {@link #baseUri} and {@link uriString}. + // This helps optimize memory usage in the same way that DASH manifests allow many URLs to be + // expressed concisely in the form of a single BaseURL and many relative paths. Note that this + // optimization relies on the same {@code Uri} being passed as the {@link #baseUri} to many + // instances of this class. + private final Uri baseUri; + private final String stringUri; + + private int hashCode; + + /** + * Constructs an ranged uri. + *

+ * The uri is built according to the following rules: + *

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

+ * 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 segmentList; - - public SegmentedRepresentation(String contentId, Format format, Uri uri, long initializationStart, - long initializationEnd, long indexStart, long indexEnd, long periodStart, long periodDuration, - List segmentList) { - super(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED, initializationStart, - initializationEnd, indexStart, indexEnd, periodStart, periodDuration); - this.segmentList = segmentList; - } - - public int getNumSegments() { - return segmentList.size(); - } - - public Segment getSegment(int i) { - return segmentList.get(i); - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/UrlTemplate.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/UrlTemplate.java new file mode 100644 index 0000000000..c055b460b2 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/UrlTemplate.java @@ -0,0 +1,164 @@ +/* + * 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; + +/** + * A template from which URLs can be built. + *

+ * 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 parsedAtoms = new HashSet(); parsedAtoms.add(Atom.TYPE_avc1); + parsedAtoms.add(Atom.TYPE_avc3); parsedAtoms.add(Atom.TYPE_esds); parsedAtoms.add(Atom.TYPE_hdlr); parsedAtoms.add(Atom.TYPE_mdat); @@ -140,7 +154,7 @@ public final class FragmentedMp4Extractor { CONTAINER_TYPES = Collections.unmodifiableSet(atomContainerTypes); } - private final boolean enableSmoothStreamingWorkarounds; + private final int workaroundFlags; // Parser state private final ParsableByteArray atomHeader; @@ -172,16 +186,15 @@ public final class FragmentedMp4Extractor { private TrackFragment fragmentRun; public FragmentedMp4Extractor() { - this(false); + this(0); } /** - * @param enableSmoothStreamingWorkarounds Set to true if this extractor will be used to parse - * SmoothStreaming streams. This will enable workarounds for SmoothStreaming violations of - * the ISO base media file format (ISO 14496-12). Set to false otherwise. + * @param workaroundFlags Flags to allow parsing of faulty streams. + * {@link #WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME} is currently the only flag defined. */ - public FragmentedMp4Extractor(boolean enableSmoothStreamingWorkarounds) { - this.enableSmoothStreamingWorkarounds = enableSmoothStreamingWorkarounds; + public FragmentedMp4Extractor(int workaroundFlags) { + this.workaroundFlags = workaroundFlags; parserState = STATE_READING_ATOM_HEADER; atomHeader = new ParsableByteArray(ATOM_HEADER_SIZE); containerAtoms = new Stack(); @@ -263,7 +276,8 @@ public final class FragmentedMp4Extractor { * in subsequent calls until the whole sample has been read. * * @param inputStream The input stream from which data should be read. - * @param out A {@link SampleHolder} into which the sample should be read. + * @param out A {@link SampleHolder} into which the next sample should be read. If null then + * {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached. * @return One or more of the {@code RESULT_*} flags defined in this class. * @throws ParserException If an error occurs parsing the media data. */ @@ -466,7 +480,7 @@ public final class FragmentedMp4Extractor { private void onMoofContainerAtomRead(ContainerAtom moof) { fragmentRun = new TrackFragment(); - parseMoof(track, extendsDefaults, moof, fragmentRun, enableSmoothStreamingWorkarounds); + parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags); sampleIndex = 0; lastSyncSampleIndex = 0; pendingSeekSyncSampleIndex = 0; @@ -572,11 +586,12 @@ public final class FragmentedMp4Extractor { int childStartPosition = stsd.getPosition(); int childAtomSize = stsd.readInt(); int childAtomType = stsd.readInt(); - if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_encv) { - Pair avc1 = - parseAvc1FromParent(stsd, childStartPosition, childAtomSize); - mediaFormat = avc1.first; - trackEncryptionBoxes[i] = avc1.second; + if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 + || childAtomType == Atom.TYPE_encv) { + Pair avc = + parseAvcFromParent(stsd, childStartPosition, childAtomSize); + mediaFormat = avc.first; + trackEncryptionBoxes[i] = avc.second; } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca) { Pair mp4a = parseMp4aFromParent(stsd, childStartPosition, childAtomSize); @@ -588,7 +603,7 @@ public final class FragmentedMp4Extractor { return Pair.create(mediaFormat, trackEncryptionBoxes); } - private static Pair parseAvc1FromParent(ParsableByteArray parent, + private static Pair parseAvcFromParent(ParsableByteArray parent, int position, int size) { parent.setPosition(position + ATOM_HEADER_SIZE); @@ -695,7 +710,7 @@ public final class FragmentedMp4Extractor { int childAtomSize = parent.readInt(); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_frma) { - parent.readInt(); // dataFormat. Expect TYPE_avc1 (video) or TYPE_mp4a (audio). + parent.readInt(); // dataFormat. } else if (childAtomType == Atom.TYPE_schm) { parent.skip(4); parent.readInt(); // schemeType. Expect cenc @@ -774,11 +789,11 @@ public final class FragmentedMp4Extractor { } private static void parseMoof(Track track, DefaultSampleValues extendsDefaults, - ContainerAtom moof, TrackFragment out, boolean enableSmoothStreamingWorkarounds) { + ContainerAtom moof, TrackFragment out, int workaroundFlags) { // TODO: Consider checking that the sequence number returned by parseMfhd is as expected. parseMfhd(moof.getLeafAtomOfType(Atom.TYPE_mfhd).getData()); parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf), - out, enableSmoothStreamingWorkarounds); + out, workaroundFlags); } /** @@ -796,7 +811,7 @@ public final class FragmentedMp4Extractor { * Parses a traf atom (defined in 14496-12). */ private static void parseTraf(Track track, DefaultSampleValues extendsDefaults, - ContainerAtom traf, TrackFragment out, boolean enableSmoothStreamingWorkarounds) { + ContainerAtom traf, TrackFragment out, int workaroundFlags) { LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); if (saiz != null) { parseSaiz(saiz.getData(), out); @@ -809,8 +824,7 @@ public final class FragmentedMp4Extractor { out.setSampleDescriptionIndex(fragmentHeader.sampleDescriptionIndex); LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun); - parseTrun(track, fragmentHeader, decodeTime, enableSmoothStreamingWorkarounds, trun.getData(), - out); + parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.getData(), out); LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid); if (uuid != null) { parseUuid(uuid.getData(), out); @@ -895,8 +909,7 @@ public final class FragmentedMp4Extractor { * @param out The {@TrackFragment} into which parsed data should be placed. */ private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues, - long decodeTime, boolean enableSmoothStreamingWorkarounds, ParsableByteArray trun, - TrackFragment out) { + long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) { trun.setPosition(ATOM_HEADER_SIZE); int fullAtom = trun.readInt(); int version = parseFullAtomVersion(fullAtom); @@ -926,6 +939,9 @@ public final class FragmentedMp4Extractor { long timescale = track.timescale; long cumulativeTime = decodeTime; + boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO + && ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) + == WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME); for (int i = 0; i < numberOfEntries; i++) { // Use trun values if present, otherwise tfhd, otherwise trex. int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt() @@ -934,11 +950,14 @@ public final class FragmentedMp4Extractor { int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags; if (sampleCompositionTimeOffsetsPresent) { - // Fragmented mp4 streams packaged for smooth streaming violate the BMFF spec by specifying - // the sample offset as a signed integer in conjunction with a box version of 0. int sampleOffset; - if (version == 0 && !enableSmoothStreamingWorkarounds) { - sampleOffset = trun.readUnsignedIntToInt(); + if (version == 0) { + // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in + // version 0 trun boxes, however a significant number of streams violate the spec and use + // signed integers instead. It's safe to always parse sample offsets as signed integers + // here, because unsigned integers will still be parsed correctly (unless their top bit is + // set, which is never true in practice because sample offsets are always small). + sampleOffset = trun.readInt(); } else { sampleOffset = trun.readInt(); } @@ -947,9 +966,7 @@ public final class FragmentedMp4Extractor { sampleDecodingTimeTable[i] = (int) ((cumulativeTime * 1000) / timescale); sampleSizeTable[i] = sampleSize; boolean isSync = ((sampleFlags >> 16) & 0x1) == 0; - if (track.type == Track.TYPE_VIDEO && enableSmoothStreamingWorkarounds && i != 0) { - // Fragmented mp4 streams packaged for smooth streaming violate the BMFF spec by indicating - // that every sample is a sync frame, when this is not actually the case. + if (workaroundEveryVideoFrameIsSyncFrame && i != 0) { isSync = false; } if (isSync) { @@ -1130,6 +1147,9 @@ public final class FragmentedMp4Extractor { @SuppressLint("InlinedApi") private int readSample(NonBlockingInputStream inputStream, SampleHolder out) { + if (out == null) { + return RESULT_NEED_SAMPLE_HOLDER; + } int sampleSize = fragmentRun.sampleSizeTable[sampleIndex]; ByteBuffer outputData = out.data; if (parserState == STATE_READING_SAMPLE_START) { diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java new file mode 100644 index 0000000000..f66b83293f --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java @@ -0,0 +1,558 @@ +/* + * 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 com.google.android.exoplayer.util.Assertions; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Stack; + +/** + * Default version of a basic event-driven incremental EBML parser which needs an + * {@link EbmlEventHandler} to define IDs/types and react to events. + * + *

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 masterElementsStack = new Stack(); + + /** + * Current {@link EbmlEventHandler} which is queried for element types + * and informed of element events. + */ + private EbmlEventHandler eventHandler; + + /** + * Overall state for the current element. Must be one of the {@code STATE_*} constants. + */ + private int state; + + /** + * Total bytes read since starting or the last {@link #reset()}. + */ + private long bytesRead; + + /** + * The starting byte offset of the current element being parsed. + */ + private long elementOffset; + + /** + * Holds the current element ID after {@link #elementIdState} is {@link #STATE_FINISHED_READING}. + */ + private int elementId; + + /** + * State for the ID of the current element. Must be one of the {@code STATE_*} constants. + */ + private int elementIdState; + + /** + * Holds the current element content size after {@link #elementContentSizeState} + * is {@link #STATE_FINISHED_READING}. + */ + private long elementContentSize; + + /** + * State for the content size of the current element. + * Must be one of the {@code STATE_*} constants. + */ + private int elementContentSizeState; + + /** + * State for the current variable-length integer (varint) being read into + * {@link #tempByteArray}. Must be one of the {@code STATE_*} constants. + */ + private int varintBytesState; + + /** + * Length in bytes of the current variable-length integer (varint) being read into + * {@link #tempByteArray}. + */ + private int varintBytesLength; + + /** + * Counts the number of bytes being contiguously read into either {@link #tempByteArray} or + * {@link #stringBytes}. Used to determine when all required bytes have been read across + * multiple calls. + */ + private int bytesState; + + /** + * Holds string element bytes as they're being read in. Allocated after the element content + * size is known and released after calling {@link EbmlEventHandler#onStringElement(int, String)}. + */ + private byte[] stringBytes; + + @Override + public void setEventHandler(EbmlEventHandler eventHandler) { + this.eventHandler = eventHandler; + } + + @Override + public int read(NonBlockingInputStream inputStream) { + Assertions.checkState(eventHandler != null); + while (true) { + while (!masterElementsStack.isEmpty() + && bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) { + if (!eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId)) { + return READ_RESULT_CONTINUE; + } + } + + if (state == STATE_BEGIN_READING) { + int idResult = readElementId(inputStream); + if (idResult != READ_RESULT_CONTINUE) { + return idResult; + } + int sizeResult = readElementContentSize(inputStream); + if (sizeResult != READ_RESULT_CONTINUE) { + return sizeResult; + } + state = STATE_READ_CONTENTS; + bytesState = 0; + } + + int type = eventHandler.getElementType(elementId); + switch (type) { + case TYPE_MASTER: + int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max. + masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize)); + if (!eventHandler.onMasterElementStart( + elementId, elementOffset, masterHeaderSize, elementContentSize)) { + prepareForNextElement(); + return READ_RESULT_CONTINUE; + } + break; + case TYPE_UNSIGNED_INT: + if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) { + throw new IllegalStateException("Invalid integer size " + elementContentSize); + } + int intResult = + readBytesInternal(inputStream, tempByteArray, (int) elementContentSize); + if (intResult != READ_RESULT_CONTINUE) { + return intResult; + } + long intValue = getTempByteArrayValue((int) elementContentSize, false); + if (!eventHandler.onIntegerElement(elementId, intValue)) { + prepareForNextElement(); + return READ_RESULT_CONTINUE; + } + break; + case TYPE_FLOAT: + if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES + && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) { + throw new IllegalStateException("Invalid float size " + elementContentSize); + } + int floatResult = + readBytesInternal(inputStream, tempByteArray, (int) elementContentSize); + if (floatResult != READ_RESULT_CONTINUE) { + return floatResult; + } + long valueBits = getTempByteArrayValue((int) elementContentSize, false); + double floatValue; + if (elementContentSize == VALID_FLOAT32_ELEMENT_SIZE_BYTES) { + floatValue = Float.intBitsToFloat((int) valueBits); + } else { + floatValue = Double.longBitsToDouble(valueBits); + } + if (!eventHandler.onFloatElement(elementId, floatValue)) { + prepareForNextElement(); + return READ_RESULT_CONTINUE; + } + break; + case TYPE_STRING: + if (elementContentSize > Integer.MAX_VALUE) { + throw new IllegalStateException( + "String element size " + elementContentSize + " is larger than MAX_INT"); + } + if (stringBytes == null) { + stringBytes = new byte[(int) elementContentSize]; + } + int stringResult = + readBytesInternal(inputStream, stringBytes, (int) elementContentSize); + if (stringResult != READ_RESULT_CONTINUE) { + return stringResult; + } + String stringValue = new String(stringBytes, Charset.forName("UTF-8")); + stringBytes = null; + if (!eventHandler.onStringElement(elementId, stringValue)) { + prepareForNextElement(); + return READ_RESULT_CONTINUE; + } + break; + case TYPE_BINARY: + if (elementContentSize > Integer.MAX_VALUE) { + throw new IllegalStateException( + "Binary element size " + elementContentSize + " is larger than MAX_INT"); + } + if (inputStream.getAvailableByteCount() < elementContentSize) { + return READ_RESULT_NEED_MORE_DATA; + } + int binaryHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max. + boolean keepGoing = eventHandler.onBinaryElement( + elementId, elementOffset, binaryHeaderSize, (int) elementContentSize, inputStream); + long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize; + if (expectedBytesRead != bytesRead) { + throw new IllegalStateException("Incorrect total bytes read. Expected " + + expectedBytesRead + " but actually " + bytesRead); + } + if (!keepGoing) { + prepareForNextElement(); + return READ_RESULT_CONTINUE; + } + break; + case TYPE_UNKNOWN: + if (elementContentSize > Integer.MAX_VALUE) { + throw new IllegalStateException( + "Unknown element size " + elementContentSize + " is larger than MAX_INT"); + } + int skipResult = skipBytesInternal(inputStream, (int) elementContentSize); + if (skipResult != READ_RESULT_CONTINUE) { + return skipResult; + } + break; + default: + throw new IllegalStateException("Invalid element type " + type); + } + prepareForNextElement(); + } + } + + @Override + public long getBytesRead() { + return bytesRead; + } + + @Override + public void reset() { + prepareForNextElement(); + masterElementsStack.clear(); + bytesRead = 0; + } + + @Override + public long readVarint(NonBlockingInputStream inputStream) { + varintBytesState = STATE_BEGIN_READING; + int result = readVarintBytes(inputStream); + if (result != READ_RESULT_CONTINUE) { + throw new IllegalStateException("Couldn't read varint"); + } + return getTempByteArrayValue(varintBytesLength, true); + } + + @Override + public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) { + bytesState = 0; + int result = readBytesInternal(inputStream, byteBuffer, totalBytes); + if (result != READ_RESULT_CONTINUE) { + throw new IllegalStateException("Couldn't read bytes into buffer"); + } + } + + @Override + public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) { + bytesState = 0; + int result = readBytesInternal(inputStream, byteArray, totalBytes); + if (result != READ_RESULT_CONTINUE) { + throw new IllegalStateException("Couldn't read bytes into array"); + } + } + + @Override + public void skipBytes(NonBlockingInputStream inputStream, int totalBytes) { + bytesState = 0; + int result = skipBytesInternal(inputStream, totalBytes); + if (result != READ_RESULT_CONTINUE) { + throw new IllegalStateException("Couldn't skip bytes"); + } + } + + /** + * Resets the internal state of {@link #read(NonBlockingInputStream)} so that it can start + * reading a new element from scratch. + */ + private 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 #READ_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 READ_RESULT_CONTINUE; + } + if (elementIdState == STATE_BEGIN_READING) { + varintBytesState = STATE_BEGIN_READING; + elementIdState = STATE_READ_CONTENTS; + } + int result = readVarintBytes(inputStream); + if (result != READ_RESULT_CONTINUE) { + return result; + } + // Element IDs are at most 4 bytes so cast to int now. + elementId = (int) getTempByteArrayValue(varintBytesLength, false); + elementIdState = STATE_FINISHED_READING; + return READ_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 #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: + *

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

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 masterElementsStack = new Stack(); - private final byte[] tempByteArray = new byte[8]; - - private int state; - private long bytesRead; - private long elementOffset; - private int elementId; - private int elementIdState; - private long elementContentSize; - private int elementContentSizeState; - private int varintBytesState; - private int varintBytesLength; - private int bytesState; - private byte[] stringBytes; - - /** - * Called to retrieve the type of an element ID. If {@link #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. - */ - protected abstract 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 the same master element to be nested within itself. - * - * @param id The integer ID of this element. - * @param elementOffset The byte offset where this element starts. - * @param headerSize The byte length of this element's ID and size header. - * @param contentsSize The byte length of this element's children. - * @return {@code true} if parsing should continue or {@code false} if it should stop right away. - */ - protected abstract boolean onMasterElementStart( - int id, long elementOffset, int headerSize, int contentsSize); - - /** - * 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 true} if parsing should continue or {@code false} if it should stop right away. - */ - protected abstract 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 true} if parsing should continue or {@code false} if it should stop right away. - */ - protected abstract 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 true} if parsing should continue or {@code false} if it should stop right away. - */ - protected abstract 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 true} if parsing should continue or {@code false} if it should stop right away. - */ - protected abstract 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 contentsSize} - * bytes in length. It's guaranteed that the full element contents will be immediately available - * from {@code inputStream}. - * - *

Several methods are available for reading the contents of a binary element: - *