From ea796f916cc1b1045990f63171e96e21a6c29330 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 4 Jul 2014 00:29:18 +0100 Subject: [PATCH 01/21] Enhance Cache/SimpleCache. 1. Fix SimpleCache startReadWrite asymmetry. Allow more concurrency. - startReadWrite does not have the concept of a read lock. Once a cached span is returned, the caller can do whatever it likes for as long as it wants to. This allows a read to be performed in parallel with a write that starts after it. - If there's an ongoing write, startReadWrite will block even if the return operation will be a read. So there's a weird asymmetry where reads can happen in parallel with writes, but only if the reads were started first. - This CL removes the asymmetry, by allowing a read to start even if the write lock is held. - Note that the reader needs to be prepared for the thing it's reading to disappear, but this was already the case, and will always be the case since the reader will need to handle disk read failures anyway. 2. Add isCached method. --- .../exoplayer/upstream/cache/Cache.java | 13 ++- .../exoplayer/upstream/cache/SimpleCache.java | 80 +++++++++++++++---- 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/Cache.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/Cache.java index 1c482e9fd3..a818520282 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/cache/Cache.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/Cache.java @@ -134,9 +134,8 @@ public interface Cache { * @param key The key of the data being requested. * @param position The position of the data being requested. * @return The {@link CacheSpan}. Or null if the cache entry is locked. - * @throws InterruptedException */ - CacheSpan startReadWriteNonBlocking(String key, long position) throws InterruptedException; + CacheSpan startReadWriteNonBlocking(String key, long position); /** * Obtains a cache file into which data can be written. Must only be called when holding a @@ -173,4 +172,14 @@ public interface Cache { */ void removeSpan(CacheSpan span); + /** + * Queries if a range is entirely available in the cache. + * + * @param key The cache key for the data. + * @param position The starting position of the data. + * @param length The length of the data. + * @return true if the data is available in the Cache otherwise false; + */ + boolean isCached(String key, long position, long length); + } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/SimpleCache.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/SimpleCache.java index 4a44407c3d..448645b648 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/cache/SimpleCache.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/SimpleCache.java @@ -109,26 +109,29 @@ public class SimpleCache implements Cache { public synchronized CacheSpan startReadWrite(String key, long position) throws InterruptedException { CacheSpan lookupSpan = CacheSpan.createLookup(key, position); - // Wait until no-one holds a lock for the key. - while (lockedSpans.containsKey(key)) { - wait(); + while (true) { + CacheSpan span = startReadWriteNonBlocking(lookupSpan); + if (span != null) { + return span; + } else { + // Write case, lock not available. We'll be woken up when a locked span is released (if the + // released lock is for the requested key then we'll be able to make progress) or when a + // span is added to the cache (if the span is for the requested key and covers the requested + // position, then we'll become a read and be able to make progress). + wait(); + } } - return getSpanningRegion(key, lookupSpan); } @Override - public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) - throws InterruptedException { - CacheSpan lookupSpan = CacheSpan.createLookup(key, position); - // Return null if key is locked - if (lockedSpans.containsKey(key)) { - return null; - } - return getSpanningRegion(key, lookupSpan); + public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) { + return startReadWriteNonBlocking(CacheSpan.createLookup(key, position)); } - private CacheSpan getSpanningRegion(String key, CacheSpan lookupSpan) { + private synchronized CacheSpan startReadWriteNonBlocking(CacheSpan lookupSpan) { CacheSpan spanningRegion = getSpan(lookupSpan); + + // Read case. if (spanningRegion.isCached) { CacheSpan oldCacheSpan = spanningRegion; // Remove the old span from the in-memory representation. @@ -139,10 +142,17 @@ public class SimpleCache implements Cache { // Add the updated span back into the in-memory representation. spansForKey.add(spanningRegion); notifySpanTouched(oldCacheSpan, spanningRegion); - } else { - lockedSpans.put(key, spanningRegion); + return spanningRegion; } - return spanningRegion; + + // Write case, lock available. + if (!lockedSpans.containsKey(lookupSpan.key)) { + lockedSpans.put(lookupSpan.key, spanningRegion); + return spanningRegion; + } + + // Write case, lock not available. + return null; } @Override @@ -173,6 +183,7 @@ public class SimpleCache implements Cache { return; } addSpan(span); + notifyAll(); } @Override @@ -330,4 +341,41 @@ public class SimpleCache implements Cache { evictor.onSpanTouched(this, oldSpan, newSpan); } + @Override + public synchronized boolean isCached(String key, long position, long length) { + TreeSet entries = cachedSpans.get(key); + if (entries == null) { + return false; + } + CacheSpan lookupSpan = CacheSpan.createLookup(key, position); + CacheSpan floorSpan = entries.floor(lookupSpan); + if (floorSpan == null || floorSpan.position + floorSpan.length <= position) { + // We don't have a span covering the start of the queried region. + return false; + } + long queryEndPosition = position + length; + long currentEndPosition = floorSpan.position + floorSpan.length; + if (currentEndPosition >= queryEndPosition) { + // floorSpan covers the queried region. + return true; + } + Iterator iterator = entries.tailSet(floorSpan, false).iterator(); + while (iterator.hasNext()) { + CacheSpan next = iterator.next(); + if (next.position > currentEndPosition) { + // There's a hole in the cache within the queried region. + return false; + } + // We expect currentEndPosition to always equal (next.position + next.length), but + // perform a max check anyway to guard against the existence of overlapping spans. + currentEndPosition = Math.max(currentEndPosition, next.position + next.length); + if (currentEndPosition >= queryEndPosition) { + // We've found spans covering the queried region. + return true; + } + } + // We ran out of spans before covering the queried region. + return false; + } + } From 8cad3873933239a589af8e6dcb5e526f2b012e4d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 4 Jul 2014 00:45:09 +0100 Subject: [PATCH 02/21] Fix ChunkSource disable definition. --- .../java/com/google/android/exoplayer/chunk/ChunkSource.java | 2 +- .../google/android/exoplayer/chunk/MultiTrackChunkSource.java | 2 +- .../com/google/android/exoplayer/dash/DashMp4ChunkSource.java | 2 +- .../com/google/android/exoplayer/dash/DashWebmChunkSource.java | 2 +- .../exoplayer/smoothstreaming/SmoothStreamingChunkSource.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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..c06fcb3661 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. 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..06b1ac4b5a 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; } 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..f1b1abcf4b 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 @@ -129,7 +129,7 @@ public class DashMp4ChunkSource implements ChunkSource { } @Override - public void disable(List queue) { + public void disable(List queue) { evaluator.disable(); } 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..f3e00f34c0 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 @@ -110,7 +110,7 @@ public class DashWebmChunkSource implements ChunkSource { } @Override - public void disable(List queue) { + public void disable(List queue) { evaluator.disable(); } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index ed8c1030fe..581d001de4 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -141,7 +141,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { } @Override - public void disable(List queue) { + public void disable(List queue) { // Do nothing. } From 50b276fe31985b3e6cd631363c8d23cc5616eb81 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 4 Jul 2014 00:47:44 +0100 Subject: [PATCH 03/21] Fixed issue in which MediaChunk.seekToStart might not actually seek to the start of the segment. --- .../java/com/google/android/exoplayer/chunk/MediaChunk.java | 4 +--- .../com/google/android/exoplayer/chunk/Mp4MediaChunk.java | 6 ++++++ .../android/exoplayer/chunk/SingleSampleMediaChunk.java | 5 +++++ .../com/google/android/exoplayer/chunk/WebmMediaChunk.java | 5 +++++ 4 files changed, 17 insertions(+), 3 deletions(-) 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..fd7c3a59b6 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. 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..4bd0076a6d 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 @@ -54,6 +54,12 @@ public final class Mp4MediaChunk extends MediaChunk { this.sampleOffsetUs = sampleOffsetUs; } + @Override + public void seekToStart() { + extractor.seekTo(0, false); + resetReadPosition(); + } + @Override public boolean seekTo(long positionUs, boolean allowNoop) { long seekTimeUs = positionUs + sampleOffsetUs; 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..ef7e1436a0 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 @@ -109,6 +109,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..23dfa2bf0a 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); From 563b434de26a717b54a7d1b117bbd72763d11f1f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 4 Jul 2014 00:52:19 +0100 Subject: [PATCH 04/21] Keep audio time in sync with arbitrary decodeOnly samples. AudioTrack time will go out of sync if the decodeOnly flag is set of arbitrary samples (as opposed to just those following a seek). It's a pretty obscure case and it would be weird for anyone to do it, but we should be robust against it anyway. --- .../MediaCodecAudioTrackRenderer.java | 42 ++++++++++++++----- .../exoplayer/MediaCodecTrackRenderer.java | 23 +++++----- .../MediaCodecVideoTrackRenderer.java | 16 ++++++- 3 files changed, 58 insertions(+), 23 deletions(-) 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); From 4fd4c8951865e246b128c987174f7c8ed2297a6d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 4 Jul 2014 01:04:10 +0100 Subject: [PATCH 05/21] Refactored ExoPlayer to use String-based format ids. --- .../exoplayer/demo/full/EventLogger.java | 6 ++-- .../demo/full/player/DemoPlayer.java | 11 ++++--- .../exoplayer/chunk/ChunkSampleSource.java | 10 +++--- .../android/exoplayer/chunk/Format.java | 21 ++++++++++-- .../exoplayer/chunk/FormatEvaluator.java | 2 +- .../exoplayer/dash/DashMp4ChunkSource.java | 14 ++++---- .../exoplayer/dash/DashWebmChunkSource.java | 14 ++++---- .../MediaPresentationDescriptionParser.java | 12 +------ .../exoplayer/dash/mpd/Representation.java | 2 +- .../SmoothStreamingChunkSource.java | 33 +++++++++++++------ 10 files changed, 73 insertions(+), 52 deletions(-) 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/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/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..d9f3923495 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); } @@ -295,7 +295,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { } return NOTHING_READ; } - } else if (downstreamFormat == null || downstreamFormat.id != mediaChunk.format.id) { + } else if (downstreamFormat == null || !downstreamFormat.id.equals(mediaChunk.format.id)) { notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger, mediaChunk.startTimeUs); MediaFormat format = mediaChunk.getMediaFormat(); @@ -653,7 +653,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 +724,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/Format.java b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java index d7d301404d..4aabe601dc 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 @@ -20,7 +20,7 @@ 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. @@ -37,7 +37,7 @@ public final class Format { /** * An identifier for the format. */ - public final int id; + public final String id; /** * The mime type of the format. @@ -70,6 +70,8 @@ public final class Format { public final int bandwidth; /** + * @deprecated Format identifiers are now strings. + * * @param id The format identifier. * @param mimeType The format mime type. * @param width The width of the video in pixels, or -1 for non-video formats. @@ -78,8 +80,23 @@ public final class Format { * @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. */ + @Deprecated public Format(int id, String mimeType, int width, int height, int numChannels, int audioSamplingRate, int bandwidth) { + this(String.valueOf(id), mimeType, width, height, numChannels, audioSamplingRate, bandwidth); + } + + /** + * @param id The format identifier. + * @param mimeType The format mime type. + * @param width The width of the video in pixels, or -1 for non-video formats. + * @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. + */ + public Format(String id, String mimeType, int width, int height, int numChannels, + int audioSamplingRate, int bandwidth) { this.id = id; this.mimeType = mimeType; this.width = width; 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..1b8b9d9082 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; 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 f1b1abcf4b..fb2bbd19f2 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 @@ -35,10 +35,10 @@ import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.NonBlockingInputStream; import android.util.Log; -import android.util.SparseArray; import java.io.IOException; import java.util.Arrays; +import java.util.HashMap; import java.util.List; /** @@ -64,8 +64,8 @@ public class DashMp4ChunkSource implements ChunkSource { 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 boolean lastChunkWasInitialization; @@ -92,8 +92,8 @@ public class DashMp4ChunkSource implements ChunkSource { 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.representations = new HashMap(); this.trackInfo = new TrackInfo(representations[0].format.mimeType, representations[0].periodDuration * 1000); this.evaluation = new Evaluation(); @@ -103,7 +103,7 @@ 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]); } this.maxWidth = maxWidth; @@ -152,7 +152,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; 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 f3e00f34c0..95db7488d9 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 @@ -35,10 +35,10 @@ import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.NonBlockingInputStream; import android.util.Log; -import android.util.SparseArray; import java.io.IOException; import java.util.Arrays; +import java.util.HashMap; import java.util.List; /** @@ -57,8 +57,8 @@ public class DashWebmChunkSource implements ChunkSource { 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 boolean lastChunkWasInitialization; @@ -73,8 +73,8 @@ public class DashWebmChunkSource implements ChunkSource { 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.representations = new HashMap(); this.trackInfo = new TrackInfo( representations[0].format.mimeType, representations[0].periodDuration * 1000); this.evaluation = new Evaluation(); @@ -84,7 +84,7 @@ 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 WebmExtractor()); this.representations.put(formats[i].id, representations[i]); } this.maxWidth = maxWidth; @@ -133,7 +133,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; 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..d14fd05f09 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 @@ -21,7 +21,6 @@ import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.util.MimeTypes; import android.net.Uri; -import android.util.Log; import org.xml.sax.helpers.DefaultHandler; import org.xmlpull.v1.XmlPullParser; @@ -45,8 +44,6 @@ import java.util.regex.Pattern; */ 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)?$"); @@ -214,14 +211,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { private Representation parseRepresentation(XmlPullParser xpp, String contentId, long periodStart, long periodDuration, String parentMimeType, List segmentTimelineList) 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(); - } + String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth") / 8; int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); int width = parseInt(xpp, "width"); 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..7b8b996888 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 @@ -81,7 +81,7 @@ public class Representation { /** * 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. */ diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 581d001de4..e996366ae2 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -63,7 +63,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { private final int maxHeight; private final SparseArray extractors; - private final Format[] formats; + private final SmoothStreamingFormat[] formats; /** * @param baseUrl The base URL for the streams. @@ -94,16 +94,16 @@ public class SmoothStreamingChunkSource implements ChunkSource { } int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length; - formats = new Format[trackCount]; + formats = new SmoothStreamingFormat[trackCount]; extractors = new SparseArray(); int maxWidth = 0; int maxHeight = 0; for (int i = 0; i < trackCount; i++) { int trackIndex = trackIndices != null ? trackIndices[i] : i; TrackElement trackElement = streamElement.tracks[trackIndex]; - formats[i] = new Format(trackIndex, trackElement.mimeType, trackElement.maxWidth, - trackElement.maxHeight, trackElement.numChannels, trackElement.sampleRate, - trackElement.bitrate / 8); + formats[i] = new SmoothStreamingFormat(String.valueOf(trackIndex), trackElement.mimeType, + trackElement.maxWidth, trackElement.maxHeight, trackElement.numChannels, + trackElement.sampleRate, trackElement.bitrate / 8, trackIndex); maxWidth = Math.max(maxWidth, trackElement.maxWidth); maxHeight = Math.max(maxHeight, trackElement.maxHeight); @@ -155,14 +155,14 @@ public class SmoothStreamingChunkSource implements ChunkSource { long playbackPositionUs, ChunkOperationHolder out) { evaluation.queueSize = queue.size(); formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation); - Format selectedFormat = evaluation.format; + SmoothStreamingFormat selectedFormat = (SmoothStreamingFormat) evaluation.format; out.queueSize = evaluation.queueSize; if (selectedFormat == null) { out.chunk = null; return; } else if (out.queueSize == queue.size() && out.chunk != null - && out.chunk.format.id == evaluation.format.id) { + && out.chunk.format.id.equals(evaluation.format.id)) { // We already have a chunk, and the evaluation hasn't changed either the format or the size // of the queue. Do nothing. return; @@ -181,11 +181,12 @@ public class SmoothStreamingChunkSource implements ChunkSource { } boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1; - String requestUrl = streamElement.buildRequestUrl(selectedFormat.id, nextChunkIndex); + String requestUrl = streamElement.buildRequestUrl(selectedFormat.trackIndex, + nextChunkIndex); Uri uri = Uri.parse(baseUrl + '/' + requestUrl); Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, - extractors.get(selectedFormat.id), dataSource, nextChunkIndex, isLastChunk, - streamElement.getStartTimeUs(nextChunkIndex), + extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, nextChunkIndex, + isLastChunk, streamElement.getStartTimeUs(nextChunkIndex), isLastChunk ? -1 : streamElement.getStartTimeUs(nextChunkIndex + 1), 0); out.chunk = mediaChunk; } @@ -254,4 +255,16 @@ public class SmoothStreamingChunkSource implements ChunkSource { data[secondPosition] = temp; } + private static final class SmoothStreamingFormat extends Format { + + public final int trackIndex; + + public SmoothStreamingFormat(String id, String mimeType, int width, int height, + int numChannels, int audioSamplingRate, int bandwidth, int trackIndex) { + super(id, mimeType, width, height, numChannels, audioSamplingRate, bandwidth); + this.trackIndex = trackIndex; + } + + } + } From 47c0bbd6b57bc26b92dbb5ec0fad514db46f7392 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 4 Jul 2014 01:06:20 +0100 Subject: [PATCH 06/21] Propagate erors to ChunkSource implementations. This can help custom ChunkSource implementations to act on this information. For example an adaptive implementation may choose to blacklist a problematic format if loads of that format keep failing. --- .../android/exoplayer/chunk/ChunkSampleSource.java | 1 + .../com/google/android/exoplayer/chunk/ChunkSource.java | 9 +++++++++ .../android/exoplayer/chunk/MultiTrackChunkSource.java | 5 +++++ .../android/exoplayer/dash/DashMp4ChunkSource.java | 5 +++++ .../android/exoplayer/dash/DashWebmChunkSource.java | 5 +++++ .../smoothstreaming/SmoothStreamingChunkSource.java | 5 +++++ 6 files changed, 30 insertions(+) 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 d9f3923495..b874f09597 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 @@ -430,6 +430,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { currentLoadableExceptionCount++; currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); notifyUpstreamError(e); + chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e); updateLoadControl(); } 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 c06fcb3661..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 @@ -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/MultiTrackChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java index 06b1ac4b5a..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 @@ -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/dash/DashMp4ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java index fb2bbd19f2..9eafebbb8f 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 @@ -192,6 +192,11 @@ public class DashMp4ChunkSource implements ChunkSource { return null; } + @Override + public void onChunkLoadError(Chunk chunk, Exception e) { + // Do nothing. + } + private static Chunk newInitializationChunk(Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource, int trigger) { DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1, 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 95db7488d9..410f720f3c 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 @@ -173,6 +173,11 @@ public class DashWebmChunkSource implements ChunkSource { return null; } + @Override + public void onChunkLoadError(Chunk chunk, Exception e) { + // Do nothing. + } + private static Chunk newInitializationChunk(Representation representation, WebmExtractor extractor, DataSource dataSource, int trigger) { DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1, diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index e996366ae2..ebf181adf8 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -196,6 +196,11 @@ public class SmoothStreamingChunkSource implements ChunkSource { return null; } + @Override + public void onChunkLoadError(Chunk chunk, Exception e) { + // Do nothing. + } + private static MediaFormat getMediaFormat(StreamElement streamElement, int trackIndex) { TrackElement trackElement = streamElement.tracks[trackIndex]; String mimeType = trackElement.mimeType; From e4ae7e08db296d5b65b04d6951009fb16be103d2 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 4 Jul 2014 01:10:52 +0100 Subject: [PATCH 07/21] Small javadoc tweak. --- .../com/google/android/exoplayer/parser/webm/EbmlReader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..7cb6d9fc01 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 @@ -290,8 +290,8 @@ public abstract class EbmlReader { /** * Resets the entire state of the reader so that it will read a new EBML structure from scratch. - * This includes resetting {@link #bytesRead} back to 0 and discarding all pending - * {@link #onMasterElementEnd(int)} events. + * This includes resetting the value returned from {@link #getBytesRead()} to 0 and discarding + * all pending {@link #onMasterElementEnd(int)} events. */ protected final void reset() { prepareForNextElement(); From b398c594fa15d56217c5482f0ff9740c757274ec Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 7 Jul 2014 14:41:45 +0100 Subject: [PATCH 08/21] Fix minSdkVersion for demo app --- demo/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 { From 43b7efa9866f0db1bb08b1e8ff695114e8a1c303 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 7 Jul 2014 15:55:01 +0100 Subject: [PATCH 09/21] Widen support for fMP4 streams. - Add support for parsing avc3 boxes. - Make workaround for signed sample offsets in trun files always enabled. - Generalize remaining workaround into a flag, to make it easy to add additional workarounds going forward without changing the API. - Fix DataSourceStream bug where read wouldn't return -1 having fully read segment whose spec length was unbounded. --- .../android/exoplayer/parser/mp4/Atom.java | 1 + .../parser/mp4/FragmentedMp4Extractor.java | 70 +++++++++++-------- .../SmoothStreamingChunkSource.java | 3 +- .../exoplayer/upstream/DataSourceStream.java | 2 +- 4 files changed, 45 insertions(+), 31 deletions(-) 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..9c44999e80 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. */ @@ -97,6 +106,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 +150,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 +182,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(); @@ -466,7 +475,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 +581,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 +598,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 +705,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 +784,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 +806,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 +819,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 +904,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 +934,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 +945,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 +961,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) { diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index ebf181adf8..6f2a2490a2 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -110,7 +110,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { MediaFormat mediaFormat = getMediaFormat(streamElement, trackIndex); int trackType = streamElement.type == StreamElement.TYPE_VIDEO ? Track.TYPE_VIDEO : Track.TYPE_AUDIO; - FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(true); + FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( + FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME); extractor.setTrack(new Track(trackIndex, trackType, streamElement.timeScale, mediaFormat, trackEncryptionBoxes)); if (protectionElement != null) { diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java index 3dc52d2054..9060c88567 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java @@ -176,7 +176,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream */ private int read(ByteBuffer target, byte[] targetArray, int targetArrayOffset, ReadHead readHead, int readLength) { - if (readHead.position == dataSpec.length) { + if (isEndOfStream()) { return -1; } int bytesToRead = (int) Math.min(loadPosition - readHead.position, readLength); From f1213a7656070b23e0684cf6166c2731b4150dae Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 7 Jul 2014 17:06:01 +0100 Subject: [PATCH 10/21] Fix NaN comparison error. --- .../android/exoplayer/upstream/DefaultBandwidthMeter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java index 9fa124a272..4b2ed2806c 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java @@ -115,8 +115,8 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { float bytesPerSecond = accumulator * 1000 / elapsedMs; slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond); float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f); - bandwidthEstimate = bandwidthEstimateFloat == Float.NaN - ? NO_ESTIMATE : (long) bandwidthEstimateFloat; + bandwidthEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE + : (long) bandwidthEstimateFloat; notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate); } streamCount--; From 1b957268a6b52e952ece23eb3515678344c8c4d9 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 9 Jul 2014 15:34:42 +0100 Subject: [PATCH 11/21] Add utility methods for inexact ceil/floor binary searches. This change also fixes issue #5 --- .../exoplayer/dash/DashMp4ChunkSource.java | 7 +- .../exoplayer/dash/DashWebmChunkSource.java | 4 +- .../SmoothStreamingManifest.java | 6 +- .../exoplayer/text/ttml/TtmlSubtitle.java | 6 +- .../google/android/exoplayer/util/Util.java | 87 +++++++++++++++++++ 5 files changed, 97 insertions(+), 13 deletions(-) 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 9eafebbb8f..e975660497 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 @@ -33,6 +33,7 @@ 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 com.google.android.exoplayer.util.Util; import android.util.Log; @@ -170,8 +171,8 @@ public class DashMp4ChunkSource implements ChunkSource { int nextIndex; if (queue.isEmpty()) { - nextIndex = Arrays.binarySearch(extractor.getSegmentIndex().timesUs, seekPositionUs); - nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex; + nextIndex = Util.binarySearchFloor(extractor.getSegmentIndex().timesUs, seekPositionUs, + true, true); } else { nextIndex = queue.get(out.queueSize - 1).nextChunkIndex; } @@ -196,7 +197,7 @@ public class DashMp4ChunkSource implements ChunkSource { public void onChunkLoadError(Chunk chunk, Exception e) { // Do nothing. } - + private static Chunk newInitializationChunk(Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource, int trigger) { DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1, 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 410f720f3c..57714f380e 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 @@ -33,6 +33,7 @@ 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 com.google.android.exoplayer.util.Util; import android.util.Log; @@ -151,8 +152,7 @@ public class DashWebmChunkSource implements ChunkSource { int nextIndex; if (queue.isEmpty()) { - nextIndex = Arrays.binarySearch(extractor.getCues().timesUs, seekPositionUs); - nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex; + nextIndex = Util.binarySearchFloor(extractor.getCues().timesUs, seekPositionUs, true, true); } else { nextIndex = queue.get(out.queueSize - 1).nextChunkIndex; } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index 185171bfe6..d6a739ee1b 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer.smoothstreaming; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.Util; -import java.util.Arrays; import java.util.UUID; /** @@ -195,9 +195,7 @@ public class SmoothStreamingManifest { * @return The index of the corresponding chunk. */ public int getChunkIndex(long timeUs) { - long time = (timeUs * timeScale) / 1000000L; - int chunkIndex = Arrays.binarySearch(chunkStartTimes, time); - return chunkIndex < 0 ? -chunkIndex - 2 : chunkIndex; + return Util.binarySearchFloor(chunkStartTimes, (timeUs * timeScale) / 1000000L, true, true); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java index 9e7299a8d2..a0c4da091e 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java +++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java @@ -16,8 +16,7 @@ package com.google.android.exoplayer.text.ttml; import com.google.android.exoplayer.text.Subtitle; - -import java.util.Arrays; +import com.google.android.exoplayer.util.Util; /** * A representation of a TTML subtitle. @@ -41,8 +40,7 @@ public final class TtmlSubtitle implements Subtitle { @Override public int getNextEventTimeIndex(long timeUs) { - int index = Arrays.binarySearch(eventTimesUs, timeUs - startTimeUs); - index = index >= 0 ? index + 1 : ~index; + int index = Util.binarySearchCeil(eventTimesUs, timeUs - startTimeUs, false, false); return index < eventTimesUs.length ? index : -1; } diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 19b535148f..ee328ba8b0 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -19,6 +19,9 @@ import com.google.android.exoplayer.upstream.DataSource; import java.io.IOException; import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -112,4 +115,88 @@ public final class Util { return text == null ? null : text.toLowerCase(Locale.US); } + /** + * Returns the index of the largest value in an array that is less than (or optionally equal to) + * a specified key. + *

+ * The search is performed using a binary search algorithm, and so the array must be sorted. + * + * @param a The array to search. + * @param key The key being searched for. + * @param inclusive If the key is present in the array, whether to return the corresponding index. + * If false then the returned index corresponds to the largest value in the array that is + * strictly less than the key. + * @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than + * the smallest value in the array. If false then -1 will be returned. + */ + public static int binarySearchFloor(long[] a, long key, boolean inclusive, boolean stayInBounds) { + int index = Arrays.binarySearch(a, key); + index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1)); + return stayInBounds ? Math.max(0, index) : index; + } + + /** + * Returns the index of the smallest value in an array that is greater than (or optionally equal + * to) a specified key. + *

+ * The search is performed using a binary search algorithm, and so the array must be sorted. + * + * @param a The array to search. + * @param key The key being searched for. + * @param inclusive If the key is present in the array, whether to return the corresponding index. + * If false then the returned index corresponds to the smallest value in the array that is + * strictly greater than the key. + * @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the + * key is greater than the largest value in the array. If false then {@code a.length} will be + * returned. + */ + public static int binarySearchCeil(long[] a, long key, boolean inclusive, boolean stayInBounds) { + int index = Arrays.binarySearch(a, key); + index = index < 0 ? ~index : (inclusive ? index : (index + 1)); + return stayInBounds ? Math.min(a.length - 1, index) : index; + } + + /** + * Returns the index of the largest value in an list that is less than (or optionally equal to) + * a specified key. + *

+ * The search is performed using a binary search algorithm, and so the list must be sorted. + * + * @param list The list to search. + * @param key The key being searched for. + * @param inclusive If the key is present in the list, whether to return the corresponding index. + * If false then the returned index corresponds to the largest value in the list that is + * strictly less than the key. + * @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than + * the smallest value in the list. If false then -1 will be returned. + */ + public static int binarySearchFloor(List> list, T key, + boolean inclusive, boolean stayInBounds) { + int index = Collections.binarySearch(list, key); + index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1)); + return stayInBounds ? Math.max(0, index) : index; + } + + /** + * Returns the index of the smallest value in an list that is greater than (or optionally equal + * to) a specified key. + *

+ * The search is performed using a binary search algorithm, and so the list must be sorted. + * + * @param list The list to search. + * @param key The key being searched for. + * @param inclusive If the key is present in the list, whether to return the corresponding index. + * If false then the returned index corresponds to the smallest value in the list that is + * strictly greater than the key. + * @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that + * the key is greater than the largest value in the list. If false then {@code list.size()} + * will be returned. + */ + public static int binarySearchCeil(List> list, T key, + boolean inclusive, boolean stayInBounds) { + int index = Collections.binarySearch(list, key); + index = index < 0 ? ~index : (inclusive ? index : (index + 1)); + return stayInBounds ? Math.min(list.size() - 1, index) : index; + } + } From 9e16dec2f84be1192c0b1562cdc55d5314d1e8d3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 9 Jul 2014 23:15:58 +0100 Subject: [PATCH 12/21] Add support for relative baseUrls in DASH manifests. Ref: Issue #2 --- .../MediaPresentationDescriptionFetcher.java | 6 +- .../MediaPresentationDescriptionParser.java | 70 ++++++++++++------- .../SmoothStreamingManifestFetcher.java | 4 +- .../exoplayer/util/ManifestFetcher.java | 14 ++-- .../google/android/exoplayer/util/Util.java | 13 ++++ 5 files changed, 74 insertions(+), 33 deletions(-) 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 d14fd05f09..18a00f99ac 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 @@ -64,14 +64,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { * @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(); @@ -79,11 +80,12 @@ 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 { + String contentId, Uri parentBaseUrl) throws XmlPullParserException, IOException { + Uri baseUrl = parentBaseUrl; long duration = parseDurationMs(xpp, "mediaPresentationDuration"); long minBufferTime = parseDurationMs(xpp, "minBufferTime"); String typeString = xpp.getAttributeValue(null, "type"); @@ -93,8 +95,10 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { List periods = new ArrayList(); do { xpp.next(); - if (isStartTag(xpp, "Period")) { - periods.add(parsePeriod(xpp, contentId, duration)); + if (isStartTag(xpp, "BaseURL")) { + baseUrl = parseBaseUrl(xpp, parentBaseUrl); + } else if (isStartTag(xpp, "Period")) { + periods.add(parsePeriod(xpp, contentId, baseUrl, duration)); } } while (!isEndTag(xpp, "MPD")); @@ -102,8 +106,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { periods); } - private Period parsePeriod(XmlPullParser xpp, String contentId, long mediaPresentationDuration) - throws XmlPullParserException, IOException { + private Period parsePeriod(XmlPullParser xpp, String contentId, Uri parentBaseUrl, + long mediaPresentationDuration) throws XmlPullParserException, IOException { + Uri baseUrl = parentBaseUrl; int id = parseInt(xpp, "id"); long start = parseDurationMs(xpp, "start", 0); long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration); @@ -115,8 +120,10 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { long presentationTimeOffset = 0; do { xpp.next(); - if (isStartTag(xpp, "AdaptationSet")) { - adaptationSets.add(parseAdaptationSet(xpp, contentId, start, duration, + if (isStartTag(xpp, "BaseURL")) { + baseUrl = parseBaseUrl(xpp, parentBaseUrl); + } else if (isStartTag(xpp, "AdaptationSet")) { + adaptationSets.add(parseAdaptationSet(xpp, contentId, baseUrl, start, duration, segmentTimelineList)); } else if (isStartTag(xpp, "SegmentList")) { segmentStartNumber = parseInt(xpp, "startNumber"); @@ -151,9 +158,10 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { return segmentTimelineList; } - private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, long periodStart, - long periodDuration, List segmentTimelineList) + private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri parentBaseUrl, + long periodStart, long periodDuration, List segmentTimelineList) throws XmlPullParserException, IOException { + Uri baseUrl = parentBaseUrl; int id = -1; int contentType = AdaptationSet.TYPE_UNKNOWN; @@ -175,7 +183,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { do { xpp.next(); if (contentType != AdaptationSet.TYPE_UNKNOWN) { - if (isStartTag(xpp, "ContentProtection")) { + if (isStartTag(xpp, "BaseURL")) { + baseUrl = parseBaseUrl(xpp, parentBaseUrl); + } else if (isStartTag(xpp, "ContentProtection")) { if (contentProtections == null) { contentProtections = new ArrayList(); } @@ -187,8 +197,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { : "audio".equals(contentTypeString) ? AdaptationSet.TYPE_AUDIO : AdaptationSet.TYPE_UNKNOWN; } else if (isStartTag(xpp, "Representation")) { - representations.add(parseRepresentation(xpp, contentId, periodStart, periodDuration, - mimeType, segmentTimelineList)); + representations.add(parseRepresentation(xpp, contentId, baseUrl, periodStart, + periodDuration, mimeType, segmentTimelineList)); } } } while (!isEndTag(xpp, "AdaptationSet")); @@ -208,9 +218,10 @@ 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) - throws XmlPullParserException, IOException { + private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri parentBaseUrl, + long periodStart, long periodDuration, String parentMimeType, + List segmentTimelineList) throws XmlPullParserException, IOException { + Uri baseUrl = parentBaseUrl; String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth") / 8; int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); @@ -222,7 +233,6 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { mimeType = parentMimeType; } - String representationUrl = null; long indexStart = -1; long indexEnd = -1; long initializationStart = -1; @@ -232,8 +242,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { do { xpp.next(); if (isStartTag(xpp, "BaseURL")) { - xpp.next(); - representationUrl = xpp.getText(); + baseUrl = parseBaseUrl(xpp, parentBaseUrl); } else if (isStartTag(xpp, "AudioChannelConfiguration")) { numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value")); } else if (isStartTag(xpp, "SegmentBase")) { @@ -249,15 +258,14 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { } } 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, + return new Representation(contentId, -1, format, baseUrl, DataSpec.LENGTH_UNBOUNDED, initializationStart, initializationEnd, indexStart, indexEnd, periodStart, periodDuration); } else { - return new SegmentedRepresentation(contentId, format, uri, initializationStart, + return new SegmentedRepresentation(contentId, format, baseUrl, initializationStart, initializationEnd, indexStart, indexEnd, periodStart, periodDuration, segmentList); } } @@ -321,7 +329,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { 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); @@ -340,4 +348,16 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { return defaultValue; } + private static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl) + throws XmlPullParserException, IOException { + xpp.next(); + String newBaseUrlText = xpp.getText(); + Uri newBaseUri = Uri.parse(newBaseUrlText); + if (newBaseUri.isAbsolute()) { + return newBaseUri; + } else { + return parentBaseUrl.buildUpon().appendEncodedPath(newBaseUrlText).build(); + } + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java index 192020f3e9..f19a054346 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer.smoothstreaming; 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; @@ -56,7 +58,7 @@ public final class SmoothStreamingManifestFetcher extends ManifestFetcher extends AsyncTask { protected final T doInBackground(String... data) { try { contentId = data.length > 1 ? data[1] : null; - URL url = new URL(data[0]); + String urlString = data[0]; String inputEncoding = null; InputStream inputStream = null; try { - HttpURLConnection connection = configureHttpConnection(url); + Uri baseUrl = Util.parseBaseUri(urlString); + HttpURLConnection connection = configureHttpConnection(new URL(urlString)); inputStream = connection.getInputStream(); inputEncoding = connection.getContentEncoding(); - return parse(inputStream, inputEncoding, contentId); + return parse(inputStream, inputEncoding, contentId, baseUrl); } finally { if (inputStream != null) { inputStream.close(); @@ -119,11 +121,13 @@ public abstract class ManifestFetcher extends AsyncTask { * @param stream The input stream to read. * @param inputEncoding The encoding of the input stream. * @param contentId The content id of the media. + * @param baseUrl Required where the manifest contains urls that are relative to a base url. May + * be null where this is not the case. * @throws IOException If an error occurred loading the data. * @throws ParserException If an error occurred parsing the loaded data. */ - protected abstract T parse(InputStream stream, String inputEncoding, String contentId) throws - IOException, ParserException; + protected abstract T parse(InputStream stream, String inputEncoding, String contentId, + Uri baseUrl) throws IOException, ParserException; private HttpURLConnection configureHttpConnection(URL url) throws IOException { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index ee328ba8b0..5ebd400133 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer.util; import com.google.android.exoplayer.upstream.DataSource; +import android.net.Uri; + import java.io.IOException; import java.net.URL; import java.util.Arrays; @@ -115,6 +117,17 @@ public final class Util { return text == null ? null : text.toLowerCase(Locale.US); } + /** + * Like {@link Uri#parse(String)}, but discards the part of the uri that follows the final + * forward slash. + * + * @param uriString An RFC 2396-compliant, encoded uri. + * @return The parsed base uri. + */ + public static Uri parseBaseUri(String uriString) { + return Uri.parse(uriString.substring(0, uriString.lastIndexOf('/'))); + } + /** * Returns the index of the largest value in an array that is less than (or optionally equal to) * a specified key. From 686ac2a6f5200572a97c66a6e5e325a4e46a31af Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 9 Jul 2014 23:20:23 +0100 Subject: [PATCH 13/21] Refactor WebM extractor. --- .../exoplayer/dash/DashWebmChunkSource.java | 3 +- .../parser/webm/DefaultEbmlReader.java | 558 ++++++++++++++++++ .../parser/webm/DefaultWebmExtractor.java | 386 ++++++++++++ .../parser/webm/EbmlEventHandler.java | 119 ++++ .../exoplayer/parser/webm/EbmlReader.java | 518 ++-------------- .../exoplayer/parser/webm/WebmExtractor.java | 360 +---------- 6 files changed, 1122 insertions(+), 822 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java create mode 100644 library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java create mode 100644 library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java 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 57714f380e..24173d3617 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 @@ -29,6 +29,7 @@ import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.WebmMediaChunk; 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; @@ -85,7 +86,7 @@ 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.put(formats[i].id, new WebmExtractor()); + extractors.put(formats[i].id, new DefaultWebmExtractor()); this.representations.put(formats[i].id, representations[i]); } this.maxWidth = maxWidth; 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..b0b0936fe1 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java @@ -0,0 +1,386 @@ +/* + * 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, EbmlEventHandler { + + 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(this); + 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; + } + + @Override + public int getElementType(int id) { + switch (id) { + case ID_EBML: + case ID_SEGMENT: + case ID_INFO: + case ID_CLUSTER: + case ID_TRACKS: + case ID_TRACK_ENTRY: + case ID_VIDEO: + case ID_CUES: + case ID_CUE_POINT: + case ID_CUE_TRACK_POSITIONS: + return EbmlReader.TYPE_MASTER; + case ID_EBML_READ_VERSION: + case ID_DOC_TYPE_READ_VERSION: + case ID_TIMECODE_SCALE: + case ID_TIME_CODE: + case ID_PIXEL_WIDTH: + case ID_PIXEL_HEIGHT: + case ID_CUE_TIME: + case ID_CUE_CLUSTER_POSITION: + return EbmlReader.TYPE_UNSIGNED_INT; + case ID_DOC_TYPE: + case ID_CODEC_ID: + return EbmlReader.TYPE_STRING; + case ID_SIMPLE_BLOCK: + return EbmlReader.TYPE_BINARY; + case ID_DURATION: + return EbmlReader.TYPE_FLOAT; + default: + return EbmlReader.TYPE_UNKNOWN; + } + } + + @Override + public 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; + } + + @Override + public boolean onMasterElementEnd(int id) { + if (id == ID_CUES) { + finishPreparing(); + return false; + } + return true; + } + + @Override + public 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; + } + + @Override + public boolean onFloatElement(int id, double value) { + if (id == ID_DURATION) { + durationUs = scaleTimecodeToUs((long) value); + } + return true; + } + + @Override + public 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; + } + + @Override + public 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; + } + +} 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..1ddb51c589 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java @@ -0,0 +1,119 @@ +/* + * 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: + *

    + *
  • {@link EbmlReader#readVarint(NonBlockingInputStream)}. + *
  • {@link EbmlReader#readBytes(NonBlockingInputStream, byte[], int)}. + *
  • {@link EbmlReader#readBytes(NonBlockingInputStream, ByteBuffer, int)}. + *
  • {@link EbmlReader#skipBytes(NonBlockingInputStream, int)}. + *
  • {@link EbmlReader#getBytesRead()}. + * + * @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 7cb6d9fc01..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: - *

      - *
    • {@link #readVarint(NonBlockingInputStream)}. - *
    • {@link #readBytes(NonBlockingInputStream, byte[], int)}. - *
    • {@link #readBytes(NonBlockingInputStream, ByteBuffer, int)}. - *
    • {@link #skipBytes(NonBlockingInputStream, int)}. - *
    • {@link #getBytesRead()}. - * - * @param inputStream The {@link NonBlockingInputStream} from which this - * element's contents should be read. - * @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 contents. - * @return {@code true} if parsing should continue or {@code false} if it should stop right away. - */ - protected abstract boolean onBinaryElement(NonBlockingInputStream inputStream, - int id, long elementOffset, int headerSize, int contentsSize); + public void setEventHandler(EbmlEventHandler eventHandler); /** * Reads from a {@link NonBlockingInputStream} and calls event callbacks as needed. * - * @param inputStream The input stream from which data should be read. - * @return One of the {@code RESULT_*} flags defined in this class. + * @param inputStream The input stream from which data should be read + * @return One of the {@code RESULT_*} flags defined in this interface */ - protected final int read(NonBlockingInputStream inputStream) { - while (true) { - while (masterElementsStack.size() > 0 - && bytesRead >= masterElementsStack.peek().elementEndOffset) { - if (!onMasterElementEnd(masterElementsStack.pop().elementId)) { - return RESULT_CONTINUE; - } - } - - if (state == STATE_BEGIN_READING) { - final int resultId = readElementId(inputStream); - if (resultId != RESULT_CONTINUE) { - return resultId; - } - final int resultSize = readElementContentSize(inputStream); - if (resultSize != RESULT_CONTINUE) { - return resultSize; - } - state = STATE_READ_CONTENTS; - bytesState = 0; - } - - final int type = getElementType(elementId); - switch (type) { - - case TYPE_MASTER: - final int masterHeaderSize = (int) (bytesRead - elementOffset); - masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize)); - if (!onMasterElementStart( - elementId, elementOffset, masterHeaderSize, (int) elementContentSize)) { - prepareForNextElement(); - return RESULT_CONTINUE; - } - break; - - case TYPE_UNSIGNED_INT: - Assertions.checkState(elementContentSize <= 8); - final int resultInt = - readBytes(inputStream, null, tempByteArray, (int) elementContentSize); - if (resultInt != RESULT_CONTINUE) { - return resultInt; - } - final long intValue = parseTempByteArray((int) elementContentSize, false); - if (!onIntegerElement(elementId, intValue)) { - prepareForNextElement(); - return RESULT_CONTINUE; - } - break; - - case TYPE_FLOAT: - Assertions.checkState(elementContentSize == 4 || elementContentSize == 8); - final int resultFloat = - readBytes(inputStream, null, tempByteArray, (int) elementContentSize); - if (resultFloat != RESULT_CONTINUE) { - return resultFloat; - } - final long valueBits = parseTempByteArray((int) elementContentSize, false); - final double floatValue; - if (elementContentSize == 4) { - floatValue = Float.intBitsToFloat((int) valueBits); - } else { - floatValue = Double.longBitsToDouble(valueBits); - } - if (!onFloatElement(elementId, floatValue)) { - prepareForNextElement(); - return RESULT_CONTINUE; - } - break; - - case TYPE_STRING: - if (stringBytes == null) { - stringBytes = new byte[(int) elementContentSize]; - } - final int resultString = - readBytes(inputStream, null, stringBytes, (int) elementContentSize); - if (resultString != RESULT_CONTINUE) { - return resultString; - } - final String stringValue = new String(stringBytes, Charset.forName("UTF-8")); - stringBytes = null; - if (!onStringElement(elementId, stringValue)) { - prepareForNextElement(); - return RESULT_CONTINUE; - } - break; - - case TYPE_BINARY: - if (inputStream.getAvailableByteCount() < elementContentSize) { - return RESULT_NEED_MORE_DATA; - } - final int binaryHeaderSize = (int) (bytesRead - elementOffset); - final boolean keepGoing = onBinaryElement( - inputStream, elementId, elementOffset, binaryHeaderSize, (int) elementContentSize); - Assertions.checkState(elementOffset + binaryHeaderSize + elementContentSize == bytesRead); - if (!keepGoing) { - prepareForNextElement(); - return RESULT_CONTINUE; - } - break; - - case TYPE_UNKNOWN: - // Unknown elements should be skipped. - Assertions.checkState( - readBytes(inputStream, null, null, (int) elementContentSize) == RESULT_CONTINUE); - break; - - default: - throw new IllegalStateException("Invalid element type " + type); - - } - prepareForNextElement(); - } - } + public int read(NonBlockingInputStream inputStream); /** - * @return The total number of bytes consumed by the reader since first created - * or last {@link #reset()}. + * The total number of bytes consumed by the reader since first created or last {@link #reset()}. */ - protected final long getBytesRead() { - return bytesRead; - } + public long getBytesRead(); /** * Resets the entire state of the reader so that it will read a new EBML structure from scratch. - * This includes resetting the value returned from {@link #getBytesRead()} to 0 and discarding - * all pending {@link #onMasterElementEnd(int)} events. + * + *

      This includes resetting the value returned from {@link #getBytesRead()} to 0 and discarding + * all pending {@link EbmlEventHandler#onMasterElementEnd(int)} events. */ - protected final void reset() { - prepareForNextElement(); - masterElementsStack.clear(); - bytesRead = 0; - } + public void reset(); /** * Reads, parses, and returns an EBML variable-length integer (varint) from the contents * of a binary element. * - * @param inputStream The input stream from which data should be read. - * @return The varint value at the current position of the contents of a binary element. + * @param inputStream The input stream from which data should be read + * @return The varint value at the current position of the contents of a binary element */ - protected final long readVarint(NonBlockingInputStream inputStream) { - varintBytesState = STATE_BEGIN_READING; - Assertions.checkState(readVarintBytes(inputStream) == RESULT_CONTINUE); - return parseTempByteArray(varintBytesLength, true); - } + public long readVarint(NonBlockingInputStream inputStream); /** * Reads a fixed number of bytes from the contents of a binary element into a {@link ByteBuffer}. * - * @param inputStream The input stream from which data should be read. - * @param byteBuffer The {@link ByteBuffer} to which data should be written. - * @param totalBytes The fixed number of bytes to be read and written. + * @param inputStream The input stream from which data should be read + * @param byteBuffer The {@link ByteBuffer} to which data should be written + * @param totalBytes The fixed number of bytes to be read and written */ - protected final void readBytes( - NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) { - bytesState = 0; - Assertions.checkState(readBytes(inputStream, byteBuffer, null, totalBytes) == RESULT_CONTINUE); - } + public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes); /** * Reads a fixed number of bytes from the contents of a binary element into a {@code byte[]}. * - * @param inputStream The input stream from which data should be read. - * @param byteArray The byte array to which data should be written. - * @param totalBytes The fixed number of bytes to be read and written. + * @param inputStream The input stream from which data should be read + * @param byteArray The byte array to which data should be written + * @param totalBytes The fixed number of bytes to be read and written */ - protected final void readBytes( - NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) { - bytesState = 0; - Assertions.checkState(readBytes(inputStream, null, byteArray, totalBytes) == RESULT_CONTINUE); - } + public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes); /** * Skips a fixed number of bytes from the contents of a binary element. * - * @param inputStream The input stream from which data should be skipped. - * @param totalBytes The fixed number of bytes to be skipped. + * @param inputStream The input stream from which data should be skipped + * @param totalBytes The fixed number of bytes to be skipped */ - protected final void skipBytes(NonBlockingInputStream inputStream, int totalBytes) { - bytesState = 0; - Assertions.checkState(readBytes(inputStream, null, null, totalBytes) == RESULT_CONTINUE); - } - - /** - * Resets the internal state of {@link #read(NonBlockingInputStream)} so that it can start - * reading a new element from scratch. - */ - private final void prepareForNextElement() { - state = STATE_BEGIN_READING; - elementIdState = STATE_BEGIN_READING; - elementContentSizeState = STATE_BEGIN_READING; - elementOffset = bytesRead; - } - - /** - * Reads an element ID such that reading can be stopped and started again in a later call - * if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if a full element ID - * has been read into {@link #elementId}. Reset {@link #elementIdState} to - * {@link #STATE_BEGIN_READING} before calling to indicate a new element ID should be read. - * - * @param inputStream The input stream from which an element ID should be read. - * @return One of the {@code RESULT_*} flags defined in this class. - */ - private int readElementId(NonBlockingInputStream inputStream) { - if (elementIdState == STATE_FINISHED_READING) { - return RESULT_CONTINUE; - } - if (elementIdState == STATE_BEGIN_READING) { - varintBytesState = STATE_BEGIN_READING; - elementIdState = STATE_READ_CONTENTS; - } - final int result = readVarintBytes(inputStream); - if (result != RESULT_CONTINUE) { - return result; - } - elementId = (int) parseTempByteArray(varintBytesLength, false); - elementIdState = STATE_FINISHED_READING; - return RESULT_CONTINUE; - } - - /** - * Reads an element's content size such that reading can be stopped and started again in a later - * call if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if an entire element - * size has been read into {@link #elementContentSize}. Reset {@link #elementContentSizeState} to - * {@link #STATE_BEGIN_READING} before calling to indicate a new element size should be read. - * - * @param inputStream The input stream from which an element size should be read. - * @return One of the {@code RESULT_*} flags defined in this class. - */ - private int readElementContentSize(NonBlockingInputStream inputStream) { - if (elementContentSizeState == STATE_FINISHED_READING) { - return RESULT_CONTINUE; - } - if (elementContentSizeState == STATE_BEGIN_READING) { - varintBytesState = STATE_BEGIN_READING; - elementContentSizeState = STATE_READ_CONTENTS; - } - final int result = readVarintBytes(inputStream); - if (result != RESULT_CONTINUE) { - return result; - } - elementContentSize = parseTempByteArray(varintBytesLength, true); - elementContentSizeState = STATE_FINISHED_READING; - return RESULT_CONTINUE; - } - - /** - * Reads an EBML variable-length integer (varint) such that reading can be stopped and started - * again in a later call if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if - * an entire varint has been read into {@link #tempByteArray} and the length of the varint is in - * {@link #varintBytesLength}. Reset {@link #varintBytesState} to {@link #STATE_BEGIN_READING} - * before calling to indicate a new varint should be read. - * - * @param inputStream The input stream from which a varint should be read. - * @return One of the {@code RESULT_*} flags defined in this class. - */ - private int readVarintBytes(NonBlockingInputStream inputStream) { - if (varintBytesState == STATE_FINISHED_READING) { - return RESULT_CONTINUE; - } - - // Read first byte to get length. - if (varintBytesState == STATE_BEGIN_READING) { - bytesState = 0; - final int result = readBytes(inputStream, null, tempByteArray, 1); - if (result != RESULT_CONTINUE) { - return result; - } - varintBytesState = STATE_READ_CONTENTS; - - final int firstByte = tempByteArray[0] & 0xff; - varintBytesLength = -1; - for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) { - if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) { - varintBytesLength = i + 1; - break; - } - } - if (varintBytesLength == -1) { - throw new IllegalStateException( - "No valid varint length mask found at bytesRead = " + bytesRead); - } - } - - // Read remaining bytes. - final int result = readBytes(inputStream, null, tempByteArray, varintBytesLength); - if (result != RESULT_CONTINUE) { - return result; - } - - // All bytes have been read. - return RESULT_CONTINUE; - } - - /** - * Reads a set amount of bytes into a {@link ByteBuffer}, {@code byte[]}, or nowhere (skipping - * the bytes) such that reading can be stopped and started again later if not enough bytes are - * available. Returns {@link #RESULT_CONTINUE} if all bytes have been read. Reset - * {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read. - * - *

      If both {@code byteBuffer} and {@code byteArray} are not null then bytes are only read - * into {@code byteBuffer}. - * - * @param inputStream The input stream from which bytes should be read. - * @param byteBuffer The optional {@link ByteBuffer} into which bytes should be read. - * @param byteArray The optional {@code byte[]} into which bytes should be read. - * @param totalBytes The total size of bytes to be read or skipped. - * @return One of the {@code RESULT_*} flags defined in this class. - */ - private int readBytes( - NonBlockingInputStream inputStream, ByteBuffer byteBuffer, byte[] byteArray, int totalBytes) { - if (bytesState == STATE_BEGIN_READING - && ((byteBuffer != null && totalBytes > byteBuffer.capacity()) - || (byteArray != null && totalBytes > byteArray.length))) { - throw new IllegalStateException("Byte destination not large enough"); - } - if (bytesState < totalBytes) { - final int remainingBytes = totalBytes - bytesState; - final int result; - if (byteBuffer != null) { - result = inputStream.read(byteBuffer, remainingBytes); - } else if (byteArray != null) { - result = inputStream.read(byteArray, bytesState, remainingBytes); - } else { - result = inputStream.skip(remainingBytes); - } - if (result == -1) { - return RESULT_END_OF_FILE; - } - bytesState += result; - bytesRead += result; - if (bytesState < totalBytes) { - return RESULT_NEED_MORE_DATA; - } - } - return RESULT_CONTINUE; - } - - /** - * Parses and returns the integer value currently read into the first {@code byteLength} bytes - * of {@link #tempByteArray}. EBML varint length masks can optionally be removed. - * - * @param byteLength The number of bytes to parse from {@link #tempByteArray}. - * @param removeLengthMask Removes the variable-length integer length mask from the value. - * @return The resulting integer value. This value could be up to 8-bytes so a Java long is used. - */ - private long parseTempByteArray(int byteLength, boolean removeLengthMask) { - if (removeLengthMask) { - tempByteArray[0] &= ~VARINT_LENGTH_MASKS[varintBytesLength - 1]; - } - long varint = 0; - for (int i = 0; i < byteLength; i++) { - // Shift all existing bits up one byte and add the next byte at the bottom. - varint = (varint << 8) | (tempByteArray[i] & 0xff); - } - return varint; - } - - /** - * Used in {@link #masterElementsStack} to track when the current master element ends so that - * {@link #onMasterElementEnd(int)} is called. - */ - private static final class MasterElement { - - private final int elementId; - private final long elementEndOffset; - - private MasterElement(int elementId, long elementEndOffset) { - this.elementId = elementId; - this.elementEndOffset = elementEndOffset; - } - - } + public void skipBytes(NonBlockingInputStream inputStream, int totalBytes); } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java index 49b82f4a16..4ecefe7906 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java @@ -19,97 +19,22 @@ import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.parser.SegmentIndex; import com.google.android.exoplayer.upstream.NonBlockingInputStream; -import com.google.android.exoplayer.util.LongArray; -import com.google.android.exoplayer.util.MimeTypes; - -import android.annotation.TargetApi; -import android.media.MediaExtractor; - -import java.util.Arrays; /** - * Facilitates the extraction of data from the WebM container format with a - * non-blocking, incremental parser based on {@link EbmlReader}. + * Extractor to facilitate data retrieval from the WebM container format. * *

      WebM is a subset of the EBML elements defined for Matroska. More information about EBML and * Matroska is available here. * More info about WebM is here. */ -@TargetApi(16) -public final class WebmExtractor extends EbmlReader { - - private static final String DOC_TYPE_WEBM = "webm"; - private static final String CODEC_ID_VP9 = "V_VP9"; - private static final int UNKNOWN = -1; - - // Element IDs - private static final int ID_EBML = 0x1A45DFA3; - private static final int ID_EBML_READ_VERSION = 0x42F7; - private static final int ID_DOC_TYPE = 0x4282; - private static final int ID_DOC_TYPE_READ_VERSION = 0x4285; - - private static final int ID_SEGMENT = 0x18538067; - - private static final int ID_INFO = 0x1549A966; - private static final int ID_TIMECODE_SCALE = 0x2AD7B1; - private static final int ID_DURATION = 0x4489; - - private static final int ID_CLUSTER = 0x1F43B675; - private static final int ID_TIME_CODE = 0xE7; - private static final int ID_SIMPLE_BLOCK = 0xA3; - - private static final int ID_TRACKS = 0x1654AE6B; - private static final int ID_TRACK_ENTRY = 0xAE; - private static final int ID_CODEC_ID = 0x86; - private static final int ID_VIDEO = 0xE0; - private static final int ID_PIXEL_WIDTH = 0xB0; - private static final int ID_PIXEL_HEIGHT = 0xBA; - - private static final int ID_CUES = 0x1C53BB6B; - private static final int ID_CUE_POINT = 0xBB; - private static final int ID_CUE_TIME = 0xB3; - private static final int ID_CUE_TRACK_POSITIONS = 0xB7; - private static final int ID_CUE_CLUSTER_POSITION = 0xF1; - - // SimpleBlock Lacing Values - private static final int LACING_NONE = 0; - private static final int LACING_XIPH = 1; - private static final int LACING_FIXED = 2; - private static final int LACING_EBML = 3; - - private final byte[] simpleBlockTimecodeAndFlags = new byte[3]; - - private SampleHolder tempSampleHolder; - private boolean sampleRead; - - private boolean prepared = false; - private long segmentStartPosition = UNKNOWN; - private long segmentEndPosition = UNKNOWN; - private long timecodeScale = 1000000L; - private long durationUs = UNKNOWN; - private int pixelWidth = UNKNOWN; - private int pixelHeight = UNKNOWN; - private int cuesByteSize = UNKNOWN; - private long clusterTimecodeUs = UNKNOWN; - private long simpleBlockTimecodeUs = UNKNOWN; - private MediaFormat format; - private SegmentIndex cues; - private LongArray cueTimesUs; - private LongArray cueClusterPositions; - - public WebmExtractor() { - cueTimesUs = new LongArray(); - cueClusterPositions = new LongArray(); - } +public interface WebmExtractor { /** * Whether the has parsed the cues and sample format from the stream. * - * @return True if the extractor is prepared. False otherwise. + * @return True if the extractor is prepared. False otherwise */ - public boolean isPrepared() { - return prepared; - } + public boolean isPrepared(); /** * Consumes data from a {@link NonBlockingInputStream}. @@ -118,289 +43,36 @@ public final class WebmExtractor extends EbmlReader { * {@code sampleHolder}. Hence the same {@link SampleHolder} instance must be passed * in subsequent calls until the whole sample has been read. * - * @param inputStream The input stream from which data should be read. - * @param sampleHolder A {@link SampleHolder} into which the sample should be read. - * @return {@code true} if a sample has been read into the sample holder, otherwise {@code false}. + * @param inputStream The input stream from which data should be read + * @param sampleHolder A {@link SampleHolder} into which the sample should be read + * @return {@code true} if a sample has been read into the sample holder */ - public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) { - tempSampleHolder = sampleHolder; - sampleRead = false; - super.read(inputStream); - tempSampleHolder = null; - return sampleRead; - } + public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder); /** * Seeks to a position before or equal to the requested time. * - * @param seekTimeUs The desired seek time in microseconds. + * @param seekTimeUs The desired seek time in microseconds * @param allowNoop Allow the seek operation to do nothing if the seek time is in the current * segment, is equal to or greater than the time of the current sample, and if there does not - * exist a sync frame between these two times. - * @return True if the operation resulted in a change of state. False if it was a no-op. + * exist a sync frame between these two times + * @return True if the operation resulted in a change of state. False if it was a no-op */ - public boolean seekTo(long seekTimeUs, boolean allowNoop) { - checkPrepared(); - if (allowNoop && simpleBlockTimecodeUs != UNKNOWN && seekTimeUs >= simpleBlockTimecodeUs) { - final int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs); - if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) { - return false; - } - } - reset(); - return true; - } + public boolean seekTo(long seekTimeUs, boolean allowNoop); /** * Returns the cues for the media stream. * * @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet - * prepared. + * prepared */ - public SegmentIndex getCues() { - checkPrepared(); - return cues; - } + public SegmentIndex getCues(); /** * Returns the format of the samples contained within the media stream. * - * @return The sample media format, or null if the extracted is not yet prepared. + * @return The sample media format, or null if the extracted is not yet prepared */ - public MediaFormat getFormat() { - checkPrepared(); - return format; - } - - @Override - protected int getElementType(int id) { - switch (id) { - case ID_EBML: - case ID_SEGMENT: - case ID_INFO: - case ID_CLUSTER: - case ID_TRACKS: - case ID_TRACK_ENTRY: - case ID_VIDEO: - case ID_CUES: - case ID_CUE_POINT: - case ID_CUE_TRACK_POSITIONS: - return EbmlReader.TYPE_MASTER; - case ID_EBML_READ_VERSION: - case ID_DOC_TYPE_READ_VERSION: - case ID_TIMECODE_SCALE: - case ID_TIME_CODE: - case ID_PIXEL_WIDTH: - case ID_PIXEL_HEIGHT: - case ID_CUE_TIME: - case ID_CUE_CLUSTER_POSITION: - return EbmlReader.TYPE_UNSIGNED_INT; - case ID_DOC_TYPE: - case ID_CODEC_ID: - return EbmlReader.TYPE_STRING; - case ID_SIMPLE_BLOCK: - return EbmlReader.TYPE_BINARY; - case ID_DURATION: - return EbmlReader.TYPE_FLOAT; - default: - return EbmlReader.TYPE_UNKNOWN; - } - } - - @Override - protected boolean onMasterElementStart( - int id, long elementOffset, int headerSize, int contentsSize) { - switch (id) { - case ID_SEGMENT: - if (segmentStartPosition != UNKNOWN || segmentEndPosition != UNKNOWN) { - throw new IllegalStateException("Multiple Segment elements not supported"); - } - segmentStartPosition = elementOffset + headerSize; - segmentEndPosition = elementOffset + headerSize + contentsSize; - break; - case ID_CUES: - cuesByteSize = headerSize + contentsSize; - break; - } - return true; - } - - @Override - protected boolean onMasterElementEnd(int id) { - switch (id) { - case ID_CUES: - finishPreparing(); - return false; - } - return true; - } - - @Override - protected boolean onIntegerElement(int id, long value) { - switch (id) { - case ID_EBML_READ_VERSION: - // Validate that EBMLReadVersion is supported. This extractor only supports v1. - if (value != 1) { - throw new IllegalStateException("EBMLReadVersion " + value + " not supported"); - } - break; - case ID_DOC_TYPE_READ_VERSION: - // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2. - if (value < 1 || value > 2) { - throw new IllegalStateException("DocTypeReadVersion " + value + " not supported"); - } - break; - case ID_TIMECODE_SCALE: - timecodeScale = value; - break; - case ID_PIXEL_WIDTH: - pixelWidth = (int) value; - break; - case ID_PIXEL_HEIGHT: - pixelHeight = (int) value; - break; - case ID_CUE_TIME: - cueTimesUs.add(scaleTimecodeToUs(value)); - break; - case ID_CUE_CLUSTER_POSITION: - cueClusterPositions.add(value); - break; - case ID_TIME_CODE: - clusterTimecodeUs = scaleTimecodeToUs(value); - break; - } - return true; - } - - @Override - protected boolean onFloatElement(int id, double value) { - switch (id) { - case ID_DURATION: - durationUs = scaleTimecodeToUs(value); - break; - } - return true; - } - - @Override - protected boolean onStringElement(int id, String value) { - switch (id) { - case ID_DOC_TYPE: - // Validate that DocType is supported. This extractor only supports "webm". - if (!DOC_TYPE_WEBM.equals(value)) { - throw new IllegalStateException("DocType " + value + " not supported"); - } - break; - case ID_CODEC_ID: - // Validate that CodecID is supported. This extractor only supports "V_VP9". - if (!CODEC_ID_VP9.equals(value)) { - throw new IllegalStateException("CodecID " + value + " not supported"); - } - break; - } - return true; - } - - @Override - protected boolean onBinaryElement(NonBlockingInputStream inputStream, - int id, long elementOffset, int headerSize, int contentsSize) { - switch (id) { - case ID_SIMPLE_BLOCK: - // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure - // for info about how data is organized in a SimpleBlock element. - - // Value of trackNumber is not used but needs to be read. - readVarint(inputStream); - - // Next three bytes have timecode and flags. - readBytes(inputStream, simpleBlockTimecodeAndFlags, 3); - - // First two bytes of the three are the relative timecode. - final int timecode = - (simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff); - final long timecodeUs = scaleTimecodeToUs(timecode); - - // Last byte of the three has some flags and the lacing value. - final boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80; - final boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08; - final int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1; - //final boolean discardable = (simpleBlockTimecodeAndFlags[2] & 0x01) == 0x01; // Not used. - - // Validate lacing and set info into sample holder. - switch (lacing) { - case LACING_NONE: - final long elementEndOffset = elementOffset + headerSize + contentsSize; - simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs; - tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; - tempSampleHolder.decodeOnly = invisible; - tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs; - tempSampleHolder.size = (int) (elementEndOffset - getBytesRead()); - break; - case LACING_EBML: - case LACING_FIXED: - case LACING_XIPH: - default: - throw new IllegalStateException("Lacing mode " + lacing + " not supported"); - } - - // Read video data into sample holder. - readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size); - sampleRead = true; - return false; - default: - skipBytes(inputStream, contentsSize); - } - return true; - } - - private long scaleTimecodeToUs(long unscaledTimecode) { - return (unscaledTimecode * timecodeScale) / 1000L; - } - - private long scaleTimecodeToUs(double unscaledTimecode) { - return (long) ((unscaledTimecode * timecodeScale) / 1000.0); - } - - private void checkPrepared() { - if (!prepared) { - throw new IllegalStateException("Parser not yet prepared"); - } - } - - private void finishPreparing() { - if (prepared - || segmentStartPosition == UNKNOWN || segmentEndPosition == UNKNOWN - || durationUs == UNKNOWN - || pixelWidth == UNKNOWN || pixelHeight == UNKNOWN - || cuesByteSize == UNKNOWN - || cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) { - throw new IllegalStateException("Incorrect state in finishPreparing()"); - } - - format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, - pixelHeight, null); - - final int cuePointsSize = cueTimesUs.size(); - final int sizeBytes = cuesByteSize; - final int[] sizes = new int[cuePointsSize]; - final long[] offsets = new long[cuePointsSize]; - final long[] durationsUs = new long[cuePointsSize]; - final long[] timesUs = new long[cuePointsSize]; - for (int i = 0; i < cuePointsSize; i++) { - timesUs[i] = cueTimesUs.get(i); - offsets[i] = segmentStartPosition + cueClusterPositions.get(i); - } - for (int i = 0; i < cuePointsSize - 1; i++) { - sizes[i] = (int) (offsets[i + 1] - offsets[i]); - durationsUs[i] = timesUs[i + 1] - timesUs[i]; - } - sizes[cuePointsSize - 1] = (int) (segmentEndPosition - offsets[cuePointsSize - 1]); - durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1]; - cues = new SegmentIndex(sizeBytes, sizes, offsets, durationsUs, timesUs); - cueTimesUs = null; - cueClusterPositions = null; - - prepared = true; - } + public MediaFormat getFormat(); } From 16fe6a809e97b4b348cb542a095faf49e7cee0fc Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 10 Jul 2014 12:01:12 +0100 Subject: [PATCH 14/21] More flexible mimeType handling in mpd parser. - Allow the content type of an adaptation set to be inferred from the mimeTypes of the contained representations. - Ensure the contained mimeTypes are consistent with one another, and with the adaptation set. Ref: Issue #2 --- .../MediaPresentationDescriptionParser.java | 85 +++++++++++++------ .../android/exoplayer/util/MimeTypes.java | 64 +++++++++++--- 2 files changed, 110 insertions(+), 39 deletions(-) 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 18a00f99ac..9f8e1e2c0c 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 @@ -18,9 +18,11 @@ 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.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; import android.net.Uri; +import android.text.TextUtils; import org.xml.sax.helpers.DefaultHandler; import org.xmlpull.v1.XmlPullParser; @@ -163,43 +165,32 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { throws XmlPullParserException, IOException { Uri baseUrl = parentBaseUrl; 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); List contentProtections = null; List representations = new ArrayList(); do { xpp.next(); - if (contentType != AdaptationSet.TYPE_UNKNOWN) { - if (isStartTag(xpp, "BaseURL")) { - baseUrl = parseBaseUrl(xpp, parentBaseUrl); - } 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")); - 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, baseUrl, periodStart, - periodDuration, mimeType, segmentTimelineList)); + if (isStartTag(xpp, "BaseURL")) { + baseUrl = parseBaseUrl(xpp, parentBaseUrl); + } 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, periodStart, + periodDuration, mimeType, segmentTimelineList); + contentType = checkAdaptationSetTypeConsistency(contentType, + parseAdaptationSetTypeFromMimeType(representation.format.mimeType)); + representations.add(representation); } } while (!isEndTag(xpp, "AdaptationSet")); @@ -360,4 +351,42 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { } } + private static 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 static 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 static 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; + } + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index a9ae0aa5c5..63d5220ac1 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -20,17 +20,39 @@ package com.google.android.exoplayer.util; */ public class MimeTypes { - public static final String VIDEO_MP4 = "video/mp4"; - public static final String VIDEO_WEBM = "video/webm"; - public static final String VIDEO_H264 = "video/avc"; - public static final String VIDEO_VP9 = "video/x-vnd.on2.vp9"; - public static final String AUDIO_MP4 = "audio/mp4"; - public static final String AUDIO_AAC = "audio/mp4a-latm"; - public static final String TEXT_VTT = "text/vtt"; - public static final String APPLICATION_TTML = "application/ttml+xml"; + public static final String BASE_TYPE_VIDEO = "video"; + public static final String BASE_TYPE_AUDIO = "audio"; + public static final String BASE_TYPE_TEXT = "text"; + public static final String BASE_TYPE_APPLICATION = "application"; + + public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4"; + public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm"; + public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc"; + public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9"; + + public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4"; + public static final String AUDIO_AAC = BASE_TYPE_AUDIO + "/mp4a-latm"; + + public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; + + public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml"; private MimeTypes() {} + /** + * Returns the top-level type of {@code mimeType}. + * + * @param mimeType The mimeType whose top-level type is required. + * @return The top-level type. + */ + public static String getTopLevelType(String mimeType) { + int indexOfSlash = mimeType.indexOf('/'); + if (indexOfSlash == -1) { + throw new IllegalArgumentException("Invalid mime type: " + mimeType); + } + return mimeType.substring(0, indexOfSlash); + } + /** * Whether the top-level type of {@code mimeType} is audio. * @@ -38,7 +60,7 @@ public class MimeTypes { * @return Whether the top level type is audio. */ public static boolean isAudio(String mimeType) { - return mimeType.startsWith("audio/"); + return getTopLevelType(mimeType).equals(BASE_TYPE_AUDIO); } /** @@ -48,7 +70,7 @@ public class MimeTypes { * @return Whether the top level type is video. */ public static boolean isVideo(String mimeType) { - return mimeType.startsWith("video/"); + return getTopLevelType(mimeType).equals(BASE_TYPE_VIDEO); } /** @@ -58,7 +80,27 @@ public class MimeTypes { * @return Whether the top level type is text. */ public static boolean isText(String mimeType) { - return mimeType.startsWith("text/"); + return getTopLevelType(mimeType).equals(BASE_TYPE_TEXT); + } + + /** + * Whether the top-level type of {@code mimeType} is application. + * + * @param mimeType The mimeType to test. + * @return Whether the top level type is application. + */ + public static boolean isApplication(String mimeType) { + return getTopLevelType(mimeType).equals(BASE_TYPE_APPLICATION); + } + + /** + * Whether the mimeType is {@link #APPLICATION_TTML}. + * + * @param mimeType The mimeType to test. + * @return Whether the mimeType is {@link #APPLICATION_TTML}. + */ + public static boolean isTtml(String mimeType) { + return mimeType.equals(APPLICATION_TTML); } } From 4366afc273d058928b947d3e6795484eb6d7bc19 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 15 Jul 2014 12:47:08 +0100 Subject: [PATCH 15/21] Support self-contained media chunks. - Support parsing of moov atoms contained within each chunk. - Also do a small cleanup to WebM parser. --- .../android/exoplayer/SampleSource.java | 4 +- .../exoplayer/chunk/ChunkSampleSource.java | 33 ++++++--- .../android/exoplayer/chunk/Format.java | 19 ++++- .../android/exoplayer/chunk/MediaChunk.java | 18 +++++ .../exoplayer/chunk/Mp4MediaChunk.java | 45 ++++++++++-- .../chunk/SingleSampleMediaChunk.java | 5 ++ .../exoplayer/chunk/WebmMediaChunk.java | 5 ++ .../exoplayer/dash/DashMp4ChunkSource.java | 4 +- .../parser/mp4/FragmentedMp4Extractor.java | 12 ++- .../parser/webm/DefaultWebmExtractor.java | 73 +++++++++++++++---- .../SmoothStreamingChunkSource.java | 4 +- 11 files changed, 180 insertions(+), 42 deletions(-) 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 b874f09597..33a4d08a7d 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 @@ -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.equals(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 (downstreamMediaFormat == null || !downstreamMediaFormat.equals(mediaFormat)) { + chunkSource.getMaxVideoDimensions(mediaFormat); + formatHolder.format = mediaFormat; + formatHolder.drmInitData = mediaChunk.getPsshInfo(); + downstreamMediaFormat = mediaFormat; return FORMAT_READ; } 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 4aabe601dc..b33fb3aeea 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,6 +15,8 @@ */ package com.google.android.exoplayer.chunk; +import com.google.android.exoplayer.util.Assertions; + import java.util.Comparator; /** @@ -97,7 +99,7 @@ public class Format { */ public Format(String id, String mimeType, int width, int height, int numChannels, int audioSamplingRate, int bandwidth) { - this.id = id; + this.id = Assertions.checkNotNull(id); this.mimeType = mimeType; this.width = width; this.height = height; @@ -106,4 +108,19 @@ public class Format { this.bandwidth = bandwidth; } + /** + * 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/MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java index fd7c3a59b6..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 @@ -87,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. @@ -99,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. */ @@ -106,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 4bd0076a6d..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,24 +33,34 @@ 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; } @@ -70,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(); @@ -84,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/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java index ef7e1436a0..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(); 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 23dfa2bf0a..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 @@ -64,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 e975660497..4bf07c1b3a 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 @@ -231,8 +231,8 @@ public class DashMp4ChunkSource implements ChunkSource { DataSpec dataSpec = new DataSpec(representation.uri, offset, size, representation.getCacheKey()); - return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, extractor, - startTimeUs, endTimeUs, 0, nextIndex); + return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs, + endTimeUs, nextIndex, extractor, false, 0); } private static class InitializationMp4Loadable extends Chunk { 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 9c44999e80..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 @@ -83,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}; @@ -272,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. */ @@ -1142,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/DefaultWebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java index b0b0936fe1..351eff32d9 100644 --- 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 @@ -36,7 +36,7 @@ import java.util.concurrent.TimeUnit; * More info about WebM is here. */ @TargetApi(16) -public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandler { +public final class DefaultWebmExtractor implements WebmExtractor { private static final String DOC_TYPE_WEBM = "webm"; private static final String CODEC_ID_VP9 = "V_VP9"; @@ -104,7 +104,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl /* package */ DefaultWebmExtractor(EbmlReader reader) { this.reader = reader; - this.reader.setEventHandler(this); + this.reader.setEventHandler(new InnerEbmlEventHandler()); this.cueTimesUs = new LongArray(); this.cueClusterPositions = new LongArray(); } @@ -150,8 +150,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl return format; } - @Override - public int getElementType(int id) { + /* package */ int getElementType(int id) { switch (id) { case ID_EBML: case ID_SEGMENT: @@ -185,8 +184,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl } } - @Override - public boolean onMasterElementStart( + /* package */ boolean onMasterElementStart( int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) { switch (id) { case ID_SEGMENT: @@ -205,8 +203,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl return true; } - @Override - public boolean onMasterElementEnd(int id) { + /* package */ boolean onMasterElementEnd(int id) { if (id == ID_CUES) { finishPreparing(); return false; @@ -214,8 +211,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl return true; } - @Override - public boolean onIntegerElement(int id, long value) { + /* package */ boolean onIntegerElement(int id, long value) { switch (id) { case ID_EBML_READ_VERSION: // Validate that EBMLReadVersion is supported. This extractor only supports v1. @@ -253,16 +249,14 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl return true; } - @Override - public boolean onFloatElement(int id, double value) { + /* package */ boolean onFloatElement(int id, double value) { if (id == ID_DURATION) { durationUs = scaleTimecodeToUs((long) value); } return true; } - @Override - public boolean onStringElement(int id, String value) { + /* package */ boolean onStringElement(int id, String value) { switch (id) { case ID_DOC_TYPE: // Validate that DocType is supported. This extractor only supports "webm". @@ -282,8 +276,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl return true; } - @Override - public boolean onBinaryElement( + /* package */ boolean onBinaryElement( int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes, NonBlockingInputStream inputStream) { if (id == ID_SIMPLE_BLOCK) { @@ -383,4 +376,52 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl 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/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 6f2a2490a2..e8ed35d239 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -235,8 +235,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey); // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs. - return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, extractor, - chunkStartTimeUs, nextStartTimeUs, -chunkStartTimeUs, nextChunkIndex); + return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs, + nextStartTimeUs, nextChunkIndex, extractor, false, -chunkStartTimeUs); } private static byte[] getKeyId(byte[] initData) { From 058333565d3f0e25e3a4d96789dd76c3c58a1c31 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 15 Jul 2014 13:55:59 +0100 Subject: [PATCH 16/21] Use bits/sec instead of bytes/sec for format bandwidth. Why: This was a bad initial choice. Manifests typically define bandwidth in bits/sec. If you divide by 8 then you're throwing away information due to rounding. Unfortunately it turns out that SegmentTemplate based manifests require you to be able to recall the bitrate exactly (because it's substituted in during segment URL construction). Medium term: We should consider converting all our bandwidth estimation over to bits/sec as well. Note1: Also changed Period id to be a string, to match the mpd spec. Note2: Made small optimization in FormatEvaluator to not consider discarding the first chunk (durationBeforeThisSegmentUs will always be negative, and even in the error case where it's not, removing the first thunk should be an error). --- .../android/exoplayer/chunk/Format.java | 36 ++++++++----------- .../exoplayer/chunk/FormatEvaluator.java | 10 +++--- .../MediaPresentationDescriptionParser.java | 4 +-- .../android/exoplayer/dash/mpd/Period.java | 8 ++--- .../SmoothStreamingChunkSource.java | 6 ++-- 5 files changed, 28 insertions(+), 36 deletions(-) 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 b33fb3aeea..ea8344c6c2 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 @@ -31,7 +31,7 @@ public class Format { @Override public int compare(Format a, Format b) { - return b.bandwidth - a.bandwidth; + return b.bitrate - a.bitrate; } } @@ -67,45 +67,37 @@ public 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; /** - * @deprecated Format identifiers are now strings. - * * @param id The format identifier. * @param mimeType The format mime type. * @param width The width of the video in pixels, or -1 for non-video formats. * @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. - */ - @Deprecated - public Format(int id, String mimeType, int width, int height, int numChannels, - int audioSamplingRate, int bandwidth) { - this(String.valueOf(id), mimeType, width, height, numChannels, audioSamplingRate, bandwidth); - } - - /** - * @param id The format identifier. - * @param mimeType The format mime type. - * @param width The width of the video in pixels, or -1 for non-video formats. - * @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(String id, String mimeType, int width, int height, int numChannels, - int audioSamplingRate, int bandwidth) { + 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; } /** 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 1b8b9d9082..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 @@ -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/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 9f8e1e2c0c..3d216d4839 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 @@ -111,7 +111,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { private Period parsePeriod(XmlPullParser xpp, String contentId, Uri parentBaseUrl, long mediaPresentationDuration) throws XmlPullParserException, IOException { Uri baseUrl = parentBaseUrl; - int id = parseInt(xpp, "id"); + String id = xpp.getAttributeValue(null, "id"); long start = parseDurationMs(xpp, "start", 0); long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration); @@ -214,7 +214,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { List segmentTimelineList) throws XmlPullParserException, IOException { Uri baseUrl = parentBaseUrl; String id = xpp.getAttributeValue(null, "id"); - int bandwidth = parseInt(xpp, "bandwidth") / 8; + int bandwidth = parseInt(xpp, "bandwidth"); int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); int width = parseInt(xpp, "width"); int height = parseInt(xpp, "height"); 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..71294204e7 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,7 +23,7 @@ import java.util.List; */ public final class Period { - public final int id; + public final String id; public final long start; @@ -39,16 +39,16 @@ public final class Period { public final long presentationTimeOffset; - public Period(int id, long start, long duration, List adaptationSets) { + public Period(String 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, + public Period(String 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, + public Period(String id, long start, long duration, List adaptationSets, List segmentList, int segmentStartNumber, int segmentTimescale, long presentationTimeOffset) { this.id = id; diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index e8ed35d239..d6a70e0364 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -103,7 +103,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { TrackElement trackElement = streamElement.tracks[trackIndex]; formats[i] = new SmoothStreamingFormat(String.valueOf(trackIndex), trackElement.mimeType, trackElement.maxWidth, trackElement.maxHeight, trackElement.numChannels, - trackElement.sampleRate, trackElement.bitrate / 8, trackIndex); + trackElement.sampleRate, trackElement.bitrate, trackIndex); maxWidth = Math.max(maxWidth, trackElement.maxWidth); maxHeight = Math.max(maxHeight, trackElement.maxHeight); @@ -266,8 +266,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { public final int trackIndex; public SmoothStreamingFormat(String id, String mimeType, int width, int height, - int numChannels, int audioSamplingRate, int bandwidth, int trackIndex) { - super(id, mimeType, width, height, numChannels, audioSamplingRate, bandwidth); + int numChannels, int audioSamplingRate, int bitrate, int trackIndex) { + super(id, mimeType, width, height, numChannels, audioSamplingRate, bitrate); this.trackIndex = trackIndex; } From d7d14037b811187a8e1ed6b885932ceb68422609 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 15 Jul 2014 18:32:37 +0100 Subject: [PATCH 17/21] Add utility classes for SegmentTemplate/SegmentList refactor. A step towards supporting SegmentTemplate style MPDs. --- .../android/exoplayer/dash/mpd/RangedUri.java | 85 +++++++++ .../exoplayer/dash/mpd/UrlTemplate.java | 164 ++++++++++++++++++ .../parser/webm/EbmlEventHandler.java | 1 + 3 files changed, 250 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java create mode 100644 library/src/main/java/com/google/android/exoplayer/dash/mpd/UrlTemplate.java 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..43f52c0108 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java @@ -0,0 +1,85 @@ +/* + * 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; + + /** + * Constructs an ranged uri. + *

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

        + *
      • If {@code baseUri} is null or if {@code stringUri} is absolute, then {@code baseUri} is + * ignored and the url consists solely of {@code stringUri}. + *
      • If {@code stringUri} is null, then the url consists solely of {@code baseUrl}. + *
      • Otherwise, the url consists of the concatenation of {@code baseUri} and {@code stringUri}. + *
      + * + * @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; + } + +} 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/webm/EbmlEventHandler.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java index 1ddb51c589..42e26d4531 100644 --- 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 @@ -103,6 +103,7 @@ import java.nio.ByteBuffer; *

    • {@link EbmlReader#readBytes(NonBlockingInputStream, ByteBuffer, int)}. *
    • {@link EbmlReader#skipBytes(NonBlockingInputStream, int)}. *
    • {@link EbmlReader#getBytesRead()}. + *
    * * @param id The integer ID of this element * @param elementOffsetBytes The byte offset where this element starts From 87461821fe0c4d502c91a449b9578e6c13ca31ad Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 17 Jul 2014 11:28:05 +0100 Subject: [PATCH 18/21] Define DashSegmentIndex wrapper. This paves the way for SegmentTemplate and SegmentList based mpds, which will implement DashSegmentIndex directly rather than parsing an index from the media stream. - Define DashSegmentIndex. - Make use of DashSegmentIndex in chunk sources. - Define an implementation of DashSegmentIndex that wraps a SegmentIndex. - Add method that will allow Representations to return a DashSegmentIndex directly in the future. - Add support for non-contiguous index and initialization data in media streams. For the Webm case this isn't enabled yet due to extractor limitations. - Removed ability to fetch multiple chunks. This functionality does not extend properly to SegmentList and SegmentTemplate variants of DASH. --- .../android/exoplayer/demo/Samples.java | 4 + .../android/exoplayer/chunk/Format.java | 5 + .../exoplayer/dash/DashMp4ChunkSource.java | 164 +++++++++--------- .../exoplayer/dash/DashWebmChunkSource.java | 116 +++++-------- .../android/exoplayer/dash/mpd/RangedUri.java | 53 ++++++ .../exoplayer/dash/mpd/Representation.java | 32 ++++ 6 files changed, 216 insertions(+), 158 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index c2dc0ff1c7..01bf8dc527 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -129,6 +129,10 @@ package com.google.android.exoplayer.demo; + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6." + "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + new Sample("WV: 30s license duration", "f9a34cab7b05881a", + "http://dash.edgesuite.net/digitalprimates/fraunhofer/480p_video/heaac_2_0_with_video/ElephantsDream/elephants_dream_480p_heaac2_0.mpd", DemoUtil.TYPE_DASH_VOD, false, true), + + }; public static final Sample[] MISC = new Sample[] { 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 ea8344c6c2..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 @@ -100,6 +100,11 @@ public class Format { this.bandwidth = bitrate / 8; } + @Override + public int hashCode() { + return id.hashCode(); + } + /** * Implements equality based on {@link #id} only. */ 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 4bf07c1b3a..b6d5219825 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,15 +27,14 @@ 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 com.google.android.exoplayer.util.Util; -import android.util.Log; +import android.net.Uri; import java.io.IOException; import java.util.Arrays; @@ -47,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 HashMap representations; private final HashMap extractors; + private final HashMap segmentIndexes; private boolean lastChunkWasInitialization; @@ -77,23 +67,11 @@ 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 HashMap(); + this.segmentIndexes = new HashMap(); this.representations = new HashMap(); this.trackInfo = new TrackInfo(representations[0].format.mimeType, representations[0].periodDuration * 1000); @@ -106,6 +84,10 @@ public class DashMp4ChunkSource implements ChunkSource { maxHeight = Math.max(formats[i].height, maxHeight); 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; @@ -161,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 = Util.binarySearchFloor(extractor.getSegmentIndex().timesUs, seekPositionUs, - true, true); + 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; } @@ -198,75 +190,75 @@ public class DashMp4ChunkSource implements ChunkSource { // Do nothing. } - private static Chunk newInitializationChunk(Representation representation, - FragmentedMp4Extractor extractor, DataSource dataSource, int trigger) { - DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1, + 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 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(requestUri.getUri(), requestUri.start, requestUri.length, representation.getCacheKey()); - return new InitializationMp4Loadable(dataSource, dataSpec, trigger, extractor, representation); + return new InitializationMp4Loadable(dataSource, dataSpec, trigger, representation.format, + extractor, expectedExtractorResult, indexAnchor); } - 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]; - } - - DataSpec dataSpec = new DataSpec(representation.uri, offset, size, + 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, nextIndex, extractor, false, 0); + endTimeUs, nextSegmentNum, extractor, false, 0); } - private static class InitializationMp4Loadable extends Chunk { + 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 24173d3617..3c3718fcd5 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,16 +27,15 @@ 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 com.google.android.exoplayer.util.Util; -import android.util.Log; +import android.net.Uri; import java.io.IOException; import java.util.Arrays; @@ -48,34 +47,27 @@ 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 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 HashMap(); + this.segmentIndexes = new HashMap(); this.representations = new HashMap(); this.trackInfo = new TrackInfo( representations[0].format.mimeType, representations[0].periodDuration * 1000); @@ -88,6 +80,10 @@ public class DashWebmChunkSource implements ChunkSource { maxHeight = Math.max(formats[i].height, maxHeight); 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; @@ -143,28 +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 = Util.binarySearchFloor(extractor.getCues().timesUs, seekPositionUs, true, true); + 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; } @@ -179,53 +181,38 @@ public class DashWebmChunkSource implements ChunkSource { // Do nothing. } - private static Chunk newInitializationChunk(Representation representation, + private Chunk newInitializationChunk(RangedUri initializationUri, 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); + DataSpec dataSpec = new DataSpec(initializationUri.getUri(), initializationUri.start, + initializationUri.length, representation.getCacheKey()); + return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format, + extractor); } - private static Chunk newMediaChunk(Representation representation, - WebmExtractor extractor, DataSource dataSource, SegmentIndex cues, int index, - int trigger, int numSegmentsPerChunk) { - - // 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 @@ -234,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/RangedUri.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java index 43f52c0108..cd18f85599 100644 --- 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 @@ -42,6 +42,8 @@ public final class RangedUri { private final Uri baseUri; private final String stringUri; + private int hashCode; + /** * Constructs an ranged uri. *

    @@ -82,4 +84,55 @@ public final class RangedUri { 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 7b8b996888..e1d309266b 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,6 +16,7 @@ package com.google.android.exoplayer.dash.mpd; import com.google.android.exoplayer.chunk.Format; +import com.google.android.exoplayer.dash.DashSegmentIndex; import android.net.Uri; @@ -79,6 +80,37 @@ public class Representation { this.uri = uri; } + /** + * 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 new RangedUri(uri, null, initializationStart, + initializationEnd - initializationStart + 1); + } + + /** + * 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 RangedUri getIndexUri() { + return new RangedUri(uri, null, indexStart, indexEnd - indexStart + 1); + } + + /** + * 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 DashSegmentIndex getIndex() { + return null; + } + /** * Generates a cache key for the {@link Representation}, in the format * {@code contentId + "." + format.id + "." + revisionId}. From 62d17cabf0d57013593ef9a69c75f649259c33f7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 18 Jul 2014 14:30:30 +0100 Subject: [PATCH 19/21] Add support for SegmentTemplate and SegmentList mpds. Misc Notes: - Removed content type filters because some of third parties don't set content type. --- .../exoplayer/demo/SampleChooserActivity.java | 2 +- .../android/exoplayer/demo/Samples.java | 4 - .../full/player/DashVodRendererBuilder.java | 6 +- .../SmoothStreamingRendererBuilder.java | 9 +- .../demo/simple/DashVodRendererBuilder.java | 6 +- .../SmoothStreamingRendererBuilder.java | 6 +- .../exoplayer/chunk/ChunkSampleSource.java | 2 +- .../exoplayer/dash/DashMp4ChunkSource.java | 4 +- .../exoplayer/dash/DashWebmChunkSource.java | 2 +- .../MediaPresentationDescriptionParser.java | 422 +++++++++++------- .../android/exoplayer/dash/mpd/Period.java | 53 +-- .../exoplayer/dash/mpd/Representation.java | 226 ++++++++-- .../android/exoplayer/dash/mpd/Segment.java | 81 ---- .../dash/mpd/SegmentedRepresentation.java | 49 -- 14 files changed, 485 insertions(+), 387 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer/dash/mpd/Segment.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentedRepresentation.java 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/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index 01bf8dc527..c2dc0ff1c7 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -129,10 +129,6 @@ package com.google.android.exoplayer.demo; + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6." + "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), - new Sample("WV: 30s license duration", "f9a34cab7b05881a", - "http://dash.edgesuite.net/digitalprimates/fraunhofer/480p_video/heaac_2_0_with_video/ElephantsDream/elephants_dream_480p_heaac2_0.mpd", DemoUtil.TYPE_DASH_VOD, false, true), - - }; public static final Sample[] MISC = new Sample[] { 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/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/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index 33a4d08a7d..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 @@ -309,7 +309,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { } MediaFormat mediaFormat = mediaChunk.getMediaFormat(); - if (downstreamMediaFormat == null || !downstreamMediaFormat.equals(mediaFormat)) { + if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat)) { chunkSource.getMaxVideoDimensions(mediaFormat); formatHolder.format = mediaFormat; formatHolder.drmInitData = mediaChunk.getPsshInfo(); 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 b6d5219825..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 @@ -74,7 +74,7 @@ public class DashMp4ChunkSource implements ChunkSource { 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; @@ -198,7 +198,7 @@ public class DashMp4ChunkSource implements ChunkSource { 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 at once. + // the two requests together to request both at once. expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_MOOV; requestUri = initializationUri.attemptMerge(indexUri); if (requestUri != null) { 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 3c3718fcd5..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 @@ -70,7 +70,7 @@ public class DashWebmChunkSource implements ChunkSource { 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; 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 3d216d4839..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,7 +17,10 @@ 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; @@ -39,11 +42,6 @@ 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 { // Note: Does not support the date part of ISO 8601 @@ -60,6 +58,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { } } + // MPD parsing. + /** * Parses a manifest from the provided {@link InputStream}. * @@ -86,96 +86,69 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { } private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp, - String contentId, Uri parentBaseUrl) throws XmlPullParserException, IOException { - Uri baseUrl = parentBaseUrl; - 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, "BaseURL")) { - baseUrl = parseBaseUrl(xpp, parentBaseUrl); + baseUrl = parseBaseUrl(xpp, baseUrl); } else if (isStartTag(xpp, "Period")) { - periods.add(parsePeriod(xpp, contentId, baseUrl, duration)); + 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, Uri parentBaseUrl, - long mediaPresentationDuration) throws XmlPullParserException, IOException { - Uri baseUrl = parentBaseUrl; + private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs) + throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); - long start = parseDurationMs(xpp, "start", 0); - long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration); - + 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, "BaseURL")) { - baseUrl = parseBaseUrl(xpp, parentBaseUrl); + baseUrl = parseBaseUrl(xpp, baseUrl); } else if (isStartTag(xpp, "AdaptationSet")) { - adaptationSets.add(parseAdaptationSet(xpp, contentId, baseUrl, start, duration, - segmentTimelineList)); + 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, Uri parentBaseUrl, - long periodStart, long periodDuration, List segmentTimelineList) + private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri baseUrl, + long periodStartMs, long periodDurationMs, SegmentBase segmentBase) throws XmlPullParserException, IOException { - Uri baseUrl = parentBaseUrl; - int id = -1; - // TODO: Correctly handle other common attributes and elements. See 23009-1 Table 9. String mimeType = xpp.getAttributeValue(null, "mimeType"); int contentType = parseAdaptationSetTypeFromMimeType(mimeType); + int id = -1; List contentProtections = null; List representations = new ArrayList(); do { xpp.next(); if (isStartTag(xpp, "BaseURL")) { - baseUrl = parseBaseUrl(xpp, parentBaseUrl); + baseUrl = parseBaseUrl(xpp, baseUrl); } else if (isStartTag(xpp, "ContentProtection")) { if (contentProtections == null) { contentProtections = new ArrayList(); @@ -186,17 +159,62 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { contentType = checkAdaptationSetTypeConsistency(contentType, parseAdaptationSetType(xpp.getAttributeValue(null, "contentType"))); } else if (isStartTag(xpp, "Representation")) { - Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStart, - periodDuration, mimeType, segmentTimelineList); + 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. * @@ -209,90 +227,194 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { return new ContentProtection(schemeUriId, null); } - private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri parentBaseUrl, - long periodStart, long periodDuration, String parentMimeType, - List segmentTimelineList) throws XmlPullParserException, IOException { - Uri baseUrl = parentBaseUrl; + // Representation parsing. + + private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl, + long periodStartMs, long periodDurationMs, String mimeType, SegmentBase segmentBase) + throws XmlPullParserException, IOException { 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; - } - - 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")) { - baseUrl = parseBaseUrl(xpp, parentBaseUrl); + 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")); Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate, bandwidth); - if (segmentList == null) { - return new Representation(contentId, -1, format, baseUrl, DataSpec.LENGTH_UNBOUNDED, - initializationStart, initializationEnd, indexStart, indexEnd, periodStart, - periodDuration); - } else { - return new SegmentedRepresentation(contentId, format, baseUrl, 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()); } @@ -302,21 +424,7 @@ 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); } @@ -339,54 +447,38 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { return defaultValue; } - private static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl) + 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()) { - return newBaseUri; - } else { - return parentBaseUrl.buildUpon().appendEncodedPath(newBaseUrlText).build(); + if (!newBaseUri.isAbsolute()) { + newBaseUri = Uri.withAppendedPath(parentBaseUrl, newBaseUrlText); } + return newBaseUri; } - private static 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; + protected static int parseInt(XmlPullParser xpp, String name) { + return parseInt(xpp, name, -1); } - private static 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; + protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) { + String value = xpp.getAttributeValue(null, name); + return value == null ? defaultValue : Integer.parseInt(value); } - /** - * 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 static 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; - } + 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 71294204e7..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 { + /** + * 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; - + /** + * @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, start, duration, adaptationSets, null, 0, 0, 0); - } - - public Period(String id, long start, long duration, List adaptationSets, - List segmentList, int segmentStartNumber, int segmentTimescale) { - this(id, start, duration, adaptationSets, segmentList, segmentStartNumber, segmentTimescale, 0); - } - - public Period(String id, long start, long duration, List adaptationSets, - List segmentList, int segmentStartNumber, int segmentTimescale, - long presentationTimeOffset) { 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/Representation.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java index e1d309266b..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 @@ -17,13 +17,15 @@ 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. @@ -34,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 @@ -44,40 +46,62 @@ 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; } /** @@ -87,8 +111,7 @@ public class Representation { * @return A {@link RangedUri} defining the location of the initialization data, or null. */ public RangedUri getInitializationUri() { - return new RangedUri(uri, null, initializationStart, - initializationEnd - initializationStart + 1); + return initializationUri; } /** @@ -97,9 +120,7 @@ public class Representation { * * @return The location of the segment index, or null. */ - public RangedUri getIndexUri() { - return new RangedUri(uri, null, indexStart, indexEnd - indexStart + 1); - } + public abstract RangedUri getIndexUri(); /** * Gets a segment index, if the representation is able to provide one directly. Null if the @@ -107,9 +128,7 @@ public class Representation { * * @return The segment index, or null. */ - public DashSegmentIndex getIndex() { - return null; - } + public abstract DashSegmentIndex getIndex(); /** * Generates a cache key for the {@link Representation}, in the format @@ -121,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); - } - -} From edb5446440e717b7ba96e2dca765689d823ac728 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 18 Jul 2014 14:31:55 +0100 Subject: [PATCH 20/21] Bump version to 1.0.11. --- .../java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 1ed65dfb85797a48ef062552c7708aa3b9143ac6 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 18 Jul 2014 14:37:46 +0100 Subject: [PATCH 21/21] Add some additional information to README.md. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) 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