diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 98ed2f18a9..8812dbc014 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ 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..39b21b62bf 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 @@ -134,6 +134,8 @@ package com.google.android.exoplayer.demo; public static final Sample[] MISC = new Sample[] { new Sample("Dizzy", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false, true), + new Sample("Dizzy (https->http redirect)", "uid:misc:dizzy2", "https://goo.gl/MtUDEj", + DemoUtil.TYPE_OTHER, false, true), }; private Samples() {} 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 221d2c6daa..4ee13a75fc 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 @@ -28,8 +28,7 @@ import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; import com.google.android.exoplayer.chunk.MultiTrackChunkSource; -import com.google.android.exoplayer.dash.DashMp4ChunkSource; -import com.google.android.exoplayer.dash.DashWebmChunkSource; +import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher; @@ -163,14 +162,8 @@ public class DashVodRendererBuilder implements RendererBuilder, DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); ChunkSource videoChunkSource; String mimeType = videoRepresentations[0].format.mimeType; - if (mimeType.equals(MimeTypes.VIDEO_MP4)) { - videoChunkSource = new DashMp4ChunkSource(videoDataSource, - new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); - } else if (mimeType.equals(MimeTypes.VIDEO_WEBM)) { - // TODO: Figure out how to query supported vpX resolutions. For now, restrict to standard - // definition streams. - videoRepresentations = getSdRepresentations(videoRepresentations); - videoChunkSource = new DashWebmChunkSource(videoDataSource, + if (mimeType.equals(MimeTypes.VIDEO_MP4) || mimeType.equals(MimeTypes.VIDEO_WEBM)) { + videoChunkSource = new DashChunkSource(videoDataSource, new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); } else { throw new IllegalStateException("Unexpected mime type: " + mimeType); @@ -200,7 +193,7 @@ public class DashVodRendererBuilder implements RendererBuilder, Format format = representation.format; audioTrackNames[i] = format.id + " (" + format.numChannels + "ch, " + format.audioSamplingRate + "Hz)"; - audioChunkSources[i] = new DashMp4ChunkSource(audioDataSource, + audioChunkSources[i] = new DashChunkSource(audioDataSource, audioEvaluator, representation); } audioChunkSource = new MultiTrackChunkSource(audioChunkSources); 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 e3ee3d46b3..a253de873e 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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; -import com.google.android.exoplayer.dash.DashMp4ChunkSource; +import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher; @@ -116,7 +116,7 @@ import java.util.ArrayList; // Build the video renderer. DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); - ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource, + ChunkSource videoChunkSource = new DashChunkSource(videoDataSource, new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); @@ -125,7 +125,7 @@ import java.util.ArrayList; // Build the audio renderer. DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); - ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource, + ChunkSource audioChunkSource = new DashChunkSource(audioDataSource, new FormatEvaluator.FixedEvaluator(), audioRepresentation); SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index 99c378adad..3621489f3b 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -21,6 +21,12 @@ + + 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 06973db73b..cc3265d18a 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.12"; + public static final String VERSION = "1.0.13"; /** * The version of the library, expressed as an integer. @@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo { * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * corresponding integer version 1002003. */ - public static final int VERSION_INT = 1000012; + public static final int VERSION_INT = 1000013; /** * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions} diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java index 908664495f..63afbdf0f5 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -112,7 +112,7 @@ public final class FrameworkSampleSource implements SampleSource { } @Override - public int readData(int track, long playbackPositionUs, FormatHolder formatHolder, + public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder, boolean onlyReadDiscontinuity) { Assertions.checkState(prepared); Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); 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 798282dd91..dbe7ac47cc 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -27,6 +27,7 @@ import android.media.AudioTimestamp; import android.media.AudioTrack; import android.media.MediaCodec; import android.media.MediaFormat; +import android.media.audiofx.Virtualizer; import android.os.ConditionVariable; import android.os.Handler; import android.util.Log; @@ -91,6 +92,13 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { private static final long MICROS_PER_SECOND = 1000000L; + /** + * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more + * than this amount. This is a fail safe that should not be required on correctly functioning + * devices. + */ + private static final long MAX_AUDIO_TIMSTAMP_OFFSET_US = 10 * MICROS_PER_SECOND; + private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10; private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; @@ -358,9 +366,9 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { * subsequently re-enabled. *

* The default implementation is a no-op. One reason for overriding this method would be to - * instantiate and enable a {@link android.media.audiofx.Virtualizer} in order to spatialize the - * audio channels. For this use case, any {@link android.media.audiofx.Virtualizer} instances - * should be released in {@link #onDisabled()} (if not before). + * instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For + * this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()} + * (if not before). * * @param audioSessionId The audio session id. */ @@ -425,7 +433,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected boolean isReady() { - return super.isReady() || getPendingFrameCount() > 0; + return getPendingFrameCount() > 0 + || (super.isReady() && getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL); } /** @@ -500,11 +509,18 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { audioTimestampSet = audioTimestampCompat.initTimestamp(audioTrack); - if (audioTimestampSet - && (audioTimestampCompat.getNanoTime() / 1000) < audioTrackResumeSystemTimeUs) { - // The timestamp was set, but it corresponds to a time before the track was most recently - // resumed. - audioTimestampSet = false; + if (audioTimestampSet) { + // Perform sanity checks on the timestamp. + long audioTimestampUs = audioTimestampCompat.getNanoTime() / 1000; + if (audioTimestampUs < audioTrackResumeSystemTimeUs) { + // The timestamp corresponds to a time before the track was most recently resumed. + audioTimestampSet = false; + } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMSTAMP_OFFSET_US) { + // The timestamp time base is probably wrong. + audioTimestampSet = false; + Log.w(TAG, "Spurious audio timestamp: " + audioTimestampCompat.getFramePosition() + ", " + + audioTimestampUs + ", " + systemClockUs); + } } if (audioTrackGetLatencyMethod != null) { try { 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 6f7262f79a..a23f5edf49 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -78,6 +78,22 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } + /** + * Value of {@link #sourceState} when the source is not ready. + */ + protected static final int SOURCE_STATE_NOT_READY = 0; + /** + * Value of {@link #sourceState} when the source is ready and we're able to read from it. + */ + protected static final int SOURCE_STATE_READY = 1; + /** + * Value of {@link #sourceState} when the source is ready but we might not be able to read from + * it. We transition to this state when an attempt to read a sample fails despite the source + * reporting that samples are available. This can occur when the next sample to be provided by + * the source is for another renderer. + */ + protected static final int SOURCE_STATE_READY_READ_MAY_FAIL = 2; + /** * If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of * time during which {@link #isReady()} will report true regardless of whether the new codec has @@ -108,7 +124,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { private final boolean playClearSamplesWithoutKeys; private final SampleSource source; private final SampleHolder sampleHolder; - private final FormatHolder formatHolder; + private final MediaFormatHolder formatHolder; private final HashSet decodeOnlyPresentationTimestamps; private final MediaCodec.BufferInfo outputBufferInfo; private final EventListener eventListener; @@ -128,7 +144,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { private int codecReconfigurationState; private int trackIndex; - private boolean sourceIsReady; + private int sourceState; private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; @@ -158,7 +174,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { this.eventListener = eventListener; codecCounters = new CodecCounters(); sampleHolder = new SampleHolder(false); - formatHolder = new FormatHolder(); + formatHolder = new MediaFormatHolder(); decodeOnlyPresentationTimestamps = new HashSet(); outputBufferInfo = new MediaCodec.BufferInfo(); } @@ -202,7 +218,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { @Override protected void onEnabled(long timeUs, boolean joining) { source.enable(trackIndex, timeUs); - sourceIsReady = false; + sourceState = SOURCE_STATE_NOT_READY; inputStreamEnded = false; outputStreamEnded = false; waitingForKeys = false; @@ -267,9 +283,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } codecHotswapTimeMs = getState() == TrackRenderer.STATE_STARTED ? SystemClock.elapsedRealtime() : -1; - inputIndex = -1; - outputIndex = -1; - waitingForFirstSyncFrame = true; + inputIndex = -1; + outputIndex = -1; + waitingForFirstSyncFrame = true; codecCounters.codecInitCount++; } @@ -353,7 +369,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { protected void seekTo(long timeUs) throws ExoPlaybackException { currentPositionUs = timeUs; source.seekToUs(timeUs); - sourceIsReady = false; + sourceState = SOURCE_STATE_NOT_READY; inputStreamEnded = false; outputStreamEnded = false; waitingForKeys = false; @@ -372,7 +388,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { @Override protected void doSomeWork(long timeUs) throws ExoPlaybackException { try { - sourceIsReady = source.continueBuffering(timeUs); + sourceState = source.continueBuffering(timeUs) + ? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState) + : SOURCE_STATE_NOT_READY; checkForDiscontinuity(); if (format == null) { readFormat(); @@ -384,7 +402,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } if (codec != null) { while (drainOutputBuffer(timeUs)) {} - while (feedInputBuffer()) {} + if (feedInputBuffer(true)) { + while (feedInputBuffer(false)) {} + } } } codecCounters.ensureUpdated(); @@ -429,6 +449,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { codecHotswapTimeMs = -1; inputIndex = -1; outputIndex = -1; + waitingForFirstSyncFrame = true; decodeOnlyPresentationTimestamps.clear(); // Workaround for framework bugs. // See [redacted], [redacted], [redacted]. @@ -446,11 +467,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } /** + * @param firstFeed True if this is the first call to this method from the current invocation of + * {@link #doSomeWork(long)}. False otherwise. * @return True if it may be possible to feed more input data. False otherwise. * @throws IOException If an error occurs reading data from the upstream source. * @throws ExoPlaybackException If an error occurs feeding the input buffer. */ - private boolean feedInputBuffer() throws IOException, ExoPlaybackException { + private boolean feedInputBuffer(boolean firstFeed) throws IOException, ExoPlaybackException { if (inputStreamEnded) { return false; } @@ -478,6 +501,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING; } result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false); + if (firstFeed && sourceState == SOURCE_STATE_READY && result == SampleSource.NOTHING_READ) { + sourceState = SOURCE_STATE_READY_READ_MAY_FAIL; + } } if (result == SampleSource.NOTHING_READ) { @@ -594,7 +620,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { * @param formatHolder Holds the new format. * @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}. */ - private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + private void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException { MediaFormat oldFormat = format; format = formatHolder.format; drmInitData = formatHolder.drmInitData; @@ -646,7 +672,17 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { @Override protected boolean isReady() { return format != null && !waitingForKeys - && (sourceIsReady || outputIndex >= 0 || isWithinHotswapPeriod()); + && sourceState != SOURCE_STATE_NOT_READY || outputIndex >= 0 || isWithinHotswapPeriod(); + } + + /** + * Gets the source state. + * + * @return One of {@link #SOURCE_STATE_NOT_READY}, {@link #SOURCE_STATE_READY} and + * {@link #SOURCE_STATE_READY_READ_MAY_FAIL}. + */ + protected final int getSourceState() { + return sourceState; } private boolean isWithinHotswapPeriod() { 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 b941767955..0fd1f4fd47 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -235,7 +235,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { @Override protected boolean isReady() { - if (super.isReady()) { + if (super.isReady() && (renderedFirstFrame || !codecInitialized() + || getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL)) { // Ready. If we were joining then we've now joined, so clear the joining deadline. joiningDeadlineUs = -1; return true; diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index 3188e36db0..d703a72a84 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -27,7 +27,7 @@ import java.util.Collections; import java.util.List; /** - * Encapsulates the information describing the format of media data, be it audio or video. + * Defines the format of an elementary media stream. */ public class MediaFormat { diff --git a/library/src/main/java/com/google/android/exoplayer/FormatHolder.java b/library/src/main/java/com/google/android/exoplayer/MediaFormatHolder.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer/FormatHolder.java rename to library/src/main/java/com/google/android/exoplayer/MediaFormatHolder.java index 04a6d7d85f..621a0f7986 100644 --- a/library/src/main/java/com/google/android/exoplayer/FormatHolder.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormatHolder.java @@ -21,7 +21,7 @@ import java.util.UUID; /** * Holds a {@link MediaFormat} and corresponding drm scheme initialization data. */ -public final class FormatHolder { +public final class MediaFormatHolder { /** * The format of the media. 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 fc29ef1ad5..2f26d30e9a 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -80,7 +80,7 @@ public interface SampleSource { /** * Enable the specified track. This allows the track's format and samples to be read from - * {@link #readData(int, long, FormatHolder, SampleHolder, boolean)}. + * {@link #readData(int, long, MediaFormatHolder, SampleHolder, boolean)}. *

* This method should not be called until after the source has been successfully prepared. * @@ -119,7 +119,7 @@ public interface SampleSource { * * @param track The track from which to read. * @param playbackPositionUs The current playback position. - * @param formatHolder A {@link FormatHolder} object to populate in the case of a new format. + * @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new format. * @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. If * the caller requires the sample data then it must ensure that {@link SampleHolder#data} * references a valid output buffer. @@ -129,7 +129,7 @@ public interface SampleSource { * {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}. * @throws IOException If an error occurred reading from the source. */ - public int readData(int track, long playbackPositionUs, FormatHolder formatHolder, + public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException; /** 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 980de4ec23..c59b321c9c 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 @@ -16,9 +16,9 @@ package com.google.android.exoplayer.chunk; import com.google.android.exoplayer.C; -import com.google.android.exoplayer.FormatHolder; import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackInfo; @@ -267,7 +267,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { } @Override - public int readData(int track, long playbackPositionUs, FormatHolder formatHolder, + public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException { Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(track == 0); @@ -318,6 +318,9 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { } if (!mediaChunk.prepare()) { + if (currentLoadableException != null) { + throw currentLoadableException; + } return NOTHING_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 3482d160fc..b2948c06a9 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 com.google.android.exoplayer.util.Assertions; import java.util.Comparator; /** - * A format definition for streams. + * Defines the high level format of a media stream. */ public class Format { 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 01033e73f6..2793496b53 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 @@ -18,7 +18,7 @@ package com.google.android.exoplayer.chunk; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer.parser.Extractor; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.NonBlockingInputStream; @@ -32,7 +32,7 @@ import java.util.UUID; */ public final class Mp4MediaChunk extends MediaChunk { - private final FragmentedMp4Extractor extractor; + private final Extractor extractor; private final boolean maybeSelfContained; private final long sampleOffsetUs; @@ -57,7 +57,7 @@ public final class Mp4MediaChunk extends MediaChunk { */ public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, - FragmentedMp4Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) { + Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) { super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex); this.extractor = extractor; this.maybeSelfContained = maybeSelfContained; @@ -89,7 +89,7 @@ public final class Mp4MediaChunk extends MediaChunk { NonBlockingInputStream inputStream = getNonBlockingInputStream(); Assertions.checkState(inputStream != null); int result = extractor.read(inputStream, null); - prepared = (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0; + prepared = (result & Extractor.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. @@ -107,7 +107,7 @@ public final class Mp4MediaChunk extends MediaChunk { public boolean sampleAvailable() throws ParserException { NonBlockingInputStream inputStream = getNonBlockingInputStream(); int result = extractor.read(inputStream, null); - return (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0; + return (result & Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0; } @Override @@ -115,7 +115,7 @@ public final class Mp4MediaChunk extends MediaChunk { NonBlockingInputStream inputStream = getNonBlockingInputStream(); Assertions.checkState(inputStream != null); int result = extractor.read(inputStream, holder); - boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE) != 0; + boolean sampleRead = (result & Extractor.RESULT_READ_SAMPLE) != 0; if (sampleRead) { holder.timeUs -= sampleOffsetUs; } 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 deleted file mode 100644 index 4769da9772..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java +++ /dev/null @@ -1,99 +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.chunk; - -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.SampleHolder; -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.Assertions; - -import java.util.Map; -import java.util.UUID; - -/** - * A WebM {@link MediaChunk}. - */ -public final class WebmMediaChunk extends MediaChunk { - - private final WebmExtractor extractor; - - /** - * @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 nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. - */ - public WebmMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, - int trigger, WebmExtractor extractor, long startTimeUs, long endTimeUs, - int nextChunkIndex) { - super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex); - this.extractor = extractor; - } - - @Override - public void seekToStart() { - seekTo(0, false); - } - - @Override - public boolean seekTo(long positionUs, boolean allowNoop) { - boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop); - if (isDiscontinuous) { - resetReadPosition(); - } - return isDiscontinuous; - } - - @Override - public boolean prepare() { - return true; - } - - @Override - public boolean sampleAvailable() throws ParserException { - NonBlockingInputStream inputStream = getNonBlockingInputStream(); - int result = extractor.read(inputStream, null); - return (result & WebmExtractor.RESULT_NEED_SAMPLE_HOLDER) != 0; - } - - @Override - public boolean read(SampleHolder holder) { - NonBlockingInputStream inputStream = getNonBlockingInputStream(); - Assertions.checkState(inputStream != null); - int result = extractor.read(inputStream, holder); - return (result & WebmExtractor.RESULT_READ_SAMPLE) != 0; - } - - @Override - public MediaFormat getMediaFormat() { - return extractor.getFormat(); - } - - @Override - public Map getPsshInfo() { - // TODO: Add support for Pssh to WebmExtractor - return null; - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java similarity index 83% rename from library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java rename to library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 0013df15a4..997da914cb 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -29,10 +29,13 @@ 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.Extractor; import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor; +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.MimeTypes; import android.net.Uri; @@ -42,9 +45,11 @@ import java.util.HashMap; import java.util.List; /** - * An {@link ChunkSource} for Mp4 DASH streams. + * An {@link ChunkSource} for DASH streams. + *

+ * This implementation currently supports fMP4 and webm. */ -public class DashMp4ChunkSource implements ChunkSource { +public class DashChunkSource implements ChunkSource { private final TrackInfo trackInfo; private final DataSource dataSource; @@ -55,7 +60,7 @@ public class DashMp4ChunkSource implements ChunkSource { private final Format[] formats; private final HashMap representations; - private final HashMap extractors; + private final HashMap extractors; private final HashMap segmentIndexes; private boolean lastChunkWasInitialization; @@ -65,12 +70,12 @@ public class DashMp4ChunkSource implements ChunkSource { * @param evaluator Selects from the available formats. * @param representations The representations to be considered by the source. */ - public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator, + public DashChunkSource(DataSource dataSource, FormatEvaluator evaluator, Representation... representations) { this.dataSource = dataSource; this.evaluator = evaluator; this.formats = new Format[representations.length]; - this.extractors = new HashMap(); + this.extractors = new HashMap(); this.segmentIndexes = new HashMap(); this.representations = new HashMap(); this.trackInfo = new TrackInfo(representations[0].format.mimeType, @@ -82,7 +87,9 @@ 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.put(formats[i].id, new FragmentedMp4Extractor()); + Extractor extractor = formats[i].mimeType.startsWith(MimeTypes.VIDEO_WEBM) + ? new WebmExtractor() : new FragmentedMp4Extractor(); + extractors.put(formats[i].id, extractor); this.representations.put(formats[i].id, representations[i]); DashSegmentIndex segmentIndex = representations[i].getIndex(); if (segmentIndex != null) { @@ -142,7 +149,7 @@ public class DashMp4ChunkSource implements ChunkSource { } Representation selectedRepresentation = representations.get(selectedFormat.id); - FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id); + Extractor extractor = extractors.get(selectedRepresentation.format.id); RangedUri pendingInitializationUri = null; RangedUri pendingIndexUri = null; @@ -191,35 +198,39 @@ public class DashMp4ChunkSource implements ChunkSource { } private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri, - Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource, + Representation representation, Extractor extractor, DataSource dataSource, int trigger) { - int expectedExtractorResult = FragmentedMp4Extractor.RESULT_END_OF_STREAM; + int expectedExtractorResult = Extractor.RESULT_END_OF_STREAM; long indexAnchor = 0; RangedUri requestUri; if (initializationUri != null) { // It's common for initialization and index data to be stored adjacently. Attempt to merge // the two requests together to request both at once. - expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INIT; + expectedExtractorResult |= Extractor.RESULT_READ_INIT; requestUri = initializationUri.attemptMerge(indexUri); if (requestUri != null) { - expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX; - indexAnchor = indexUri.start + indexUri.length; + expectedExtractorResult |= Extractor.RESULT_READ_INDEX; + if (extractor.hasRelativeIndexOffsets()) { + indexAnchor = indexUri.start + indexUri.length; + } } else { requestUri = initializationUri; } } else { requestUri = indexUri; - indexAnchor = indexUri.start + indexUri.length; - expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX; + if (extractor.hasRelativeIndexOffsets()) { + indexAnchor = indexUri.start + indexUri.length; + } + expectedExtractorResult |= Extractor.RESULT_READ_INDEX; } DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, representation.getCacheKey()); - return new InitializationMp4Loadable(dataSource, dataSpec, trigger, representation.format, + return new InitializationLoadable(dataSource, dataSpec, trigger, representation.format, extractor, expectedExtractorResult, indexAnchor); } private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex, - FragmentedMp4Extractor extractor, DataSource dataSource, int segmentNum, int trigger) { + Extractor extractor, DataSource dataSource, int segmentNum, int trigger) { int lastSegmentNum = segmentIndex.getLastSegmentNum(); int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1; long startTimeUs = segmentIndex.getTimeUs(segmentNum); @@ -232,15 +243,15 @@ public class DashMp4ChunkSource implements ChunkSource { endTimeUs, nextSegmentNum, extractor, false, 0); } - private class InitializationMp4Loadable extends Chunk { + private class InitializationLoadable extends Chunk { - private final FragmentedMp4Extractor extractor; + private final Extractor extractor; private final int expectedExtractorResult; private final long indexAnchor; private final Uri uri; - public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger, - Format format, FragmentedMp4Extractor extractor, int expectedExtractorResult, + public InitializationLoadable(DataSource dataSource, DataSpec dataSpec, int trigger, + Format format, Extractor extractor, int expectedExtractorResult, long indexAnchor) { super(dataSource, dataSpec, format, trigger); this.extractor = extractor; @@ -256,7 +267,7 @@ public class DashMp4ChunkSource implements ChunkSource { throw new ParserException("Invalid extractor result. Expected " + expectedExtractorResult + ", got " + result); } - if ((result & FragmentedMp4Extractor.RESULT_READ_INDEX) != 0) { + if ((result & Extractor.RESULT_READ_INDEX) != 0) { segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getIndex(), 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 deleted file mode 100644 index 2f01a38120..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java +++ /dev/null @@ -1,261 +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; - -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.TrackInfo; -import com.google.android.exoplayer.chunk.Chunk; -import com.google.android.exoplayer.chunk.ChunkOperationHolder; -import com.google.android.exoplayer.chunk.ChunkSource; -import com.google.android.exoplayer.chunk.Format; -import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator; -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.webm.DefaultWebmExtractor; -import com.google.android.exoplayer.parser.webm.WebmExtractor; -import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.upstream.DataSpec; -import com.google.android.exoplayer.upstream.NonBlockingInputStream; - -import android.net.Uri; - -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; - -/** - * An {@link ChunkSource} for WebM DASH streams. - */ -public class DashWebmChunkSource implements ChunkSource { - - 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 Format[] formats; - private final HashMap representations; - private final HashMap extractors; - private final HashMap segmentIndexes; - - private boolean lastChunkWasInitialization; - - /** - * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param evaluator Selects from the available formats. - * @param representations The representations to be considered by the source. - */ - public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator, - Representation... representations) { - this.dataSource = dataSource; - this.evaluator = evaluator; - 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].periodDurationMs * 1000); - this.evaluation = new Evaluation(); - int maxWidth = 0; - int maxHeight = 0; - for (int i = 0; i < representations.length; i++) { - 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 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; - Arrays.sort(formats, new DecreasingBandwidthComparator()); - } - - @Override - public final void getMaxVideoDimensions(MediaFormat out) { - if (trackInfo.mimeType.startsWith("video")) { - out.setMaxVideoDimensions(maxWidth, maxHeight); - } - } - - @Override - public final TrackInfo getTrackInfo() { - return trackInfo; - } - - @Override - public void enable() { - evaluator.enable(); - } - - @Override - public void disable(List queue) { - evaluator.disable(); - } - - @Override - public void continueBuffering(long playbackPositionUs) { - // Do nothing - } - - @Override - public final void getChunkOperation(List queue, long seekPositionUs, - long playbackPositionUs, ChunkOperationHolder out) { - evaluation.queueSize = queue.size(); - if (evaluation.format == null || !lastChunkWasInitialization) { - evaluator.evaluate(queue, playbackPositionUs, formats, evaluation); - } - Format selectedFormat = 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.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; - } - - Representation selectedRepresentation = representations.get(selectedFormat.id); - WebmExtractor extractor = extractors.get(selectedRepresentation.format.id); - - RangedUri pendingInitializationUri = null; - RangedUri pendingIndexUri = null; - if (extractor.getFormat() == null) { - 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 nextSegmentNum; - DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id); - if (queue.isEmpty()) { - nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs); - } else { - nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex; - } - - if (nextSegmentNum == -1) { - out.chunk = null; - return; - } - - Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor, - dataSource, nextSegmentNum, evaluation.trigger); - lastChunkWasInitialization = false; - out.chunk = nextMediaChunk; - } - - @Override - public IOException getError() { - return null; - } - - @Override - public void onChunkLoadError(Chunk chunk, Exception e) { - // Do nothing. - } - - private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri, - Representation representation, WebmExtractor extractor, DataSource dataSource, - int trigger) { - int expectedExtractorResult = WebmExtractor.RESULT_END_OF_STREAM; - RangedUri requestUri; - if (initializationUri != null) { - // It's common for initialization and index data to be stored adjacently. Attempt to merge - // the two requests together to request both at once. - expectedExtractorResult |= WebmExtractor.RESULT_READ_INIT; - requestUri = initializationUri.attemptMerge(indexUri); - if (requestUri != null) { - expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX; - } else { - requestUri = initializationUri; - } - } else { - requestUri = indexUri; - expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX; - } - DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, - representation.getCacheKey()); - return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format, - extractor, expectedExtractorResult); - } - - 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, nextSegmentNum); - } - - private class InitializationWebmLoadable extends Chunk { - - private final WebmExtractor extractor; - private final int expectedExtractorResult; - private final Uri uri; - - public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger, - Format format, WebmExtractor extractor, int expectedExtractorResult) { - super(dataSource, dataSpec, format, trigger); - this.extractor = extractor; - this.expectedExtractorResult = expectedExtractorResult; - this.uri = dataSpec.uri; - } - - @Override - protected void consumeStream(NonBlockingInputStream stream) throws IOException { - int result = extractor.read(stream, null); - if (result != expectedExtractorResult) { - throw new ParserException("Invalid extractor result. Expected " - + expectedExtractorResult + ", got " + result); - } - if ((result & WebmExtractor.RESULT_READ_INDEX) != 0) { - segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getIndex(), uri, 0)); - } - } - - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java index 6d7b35a450..98e85ac40b 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java @@ -23,6 +23,8 @@ import java.util.List; */ public final class MediaPresentationDescription { + public final long availabilityStartTime; + public final long duration; public final long minBufferTime; @@ -31,14 +33,22 @@ public final class MediaPresentationDescription { public final long minUpdatePeriod; + public final long timeShiftBufferDepth; + public final List periods; - public MediaPresentationDescription(long duration, long minBufferTime, boolean dynamic, - long minUpdatePeriod, List periods) { + public final UtcTimingElement utcTiming; + + public MediaPresentationDescription(long availabilityStartTime, long duration, long minBufferTime, + boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, UtcTimingElement utcTiming, + List periods) { + this.availabilityStartTime = availabilityStartTime; this.duration = duration; this.minBufferTime = minBufferTime; this.dynamic = dynamic; this.minUpdatePeriod = minUpdatePeriod; + this.timeShiftBufferDepth = timeShiftBufferDepth; + this.utcTiming = utcTiming; this.periods = Collections.unmodifiableList(periods); } 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 da294190f7..45885cfc90 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 @@ -20,8 +20,6 @@ import com.google.android.exoplayer.util.ManifestFetcher; import android.net.Uri; -import org.xmlpull.v1.XmlPullParserException; - import java.io.IOException; import java.io.InputStream; @@ -60,11 +58,7 @@ public final class MediaPresentationDescriptionFetcher extends @Override protected MediaPresentationDescription parse(InputStream stream, String inputEncoding, String contentId, Uri baseUrl) throws IOException, ParserException { - try { - return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl); - } catch (XmlPullParserException e) { - throw new ParserException(e); - } + return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl); } } 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 2bd53d998b..0011c5d225 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 @@ -34,8 +34,13 @@ import org.xmlpull.v1.XmlPullParserFactory; import java.io.IOException; import java.io.InputStream; +import java.math.BigDecimal; +import java.text.ParseException; import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; import java.util.List; +import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -48,6 +53,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { private static final Pattern DURATION = Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$"); + private static final Pattern DATE_TIME_PATTERN = + Pattern.compile("(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" + + "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?" + + "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?"); + private final XmlPullParserFactory xmlParserFactory; public MediaPresentationDescriptionParser() { @@ -69,42 +79,57 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { * @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, Uri baseUrl) throws XmlPullParserException, - IOException, ParserException { - XmlPullParser xpp = xmlParserFactory.newPullParser(); - xpp.setInput(inputStream, inputEncoding); - int eventType = xpp.next(); - if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) { - throw new ParserException( - "inputStream does not contain a valid media presentation description"); + String inputEncoding, String contentId, Uri baseUrl) throws IOException, ParserException { + try { + XmlPullParser xpp = xmlParserFactory.newPullParser(); + xpp.setInput(inputStream, inputEncoding); + int eventType = xpp.next(); + if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) { + throw new ParserException( + "inputStream does not contain a valid media presentation description"); + } + return parseMediaPresentationDescription(xpp, contentId, baseUrl); + } catch (XmlPullParserException e) { + throw new ParserException(e); + } catch (ParseException e) { + throw new ParserException(e); } - return parseMediaPresentationDescription(xpp, contentId, baseUrl); } private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp, - String contentId, Uri baseUrl) throws XmlPullParserException, IOException { + String contentId, Uri baseUrl) throws XmlPullParserException, IOException, ParseException { + long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1); 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 minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1; + long timeShiftBufferDepthMs = (dynamic) ? parseDurationMs(xpp, "timeShiftBufferDepth", -1) : -1; + UtcTimingElement utcTiming = null; List periods = new ArrayList(); do { xpp.next(); if (isStartTag(xpp, "BaseURL")) { baseUrl = parseBaseUrl(xpp, baseUrl); + } else if (isStartTag(xpp, "UTCTiming")) { + utcTiming = parseUtcTiming(xpp); } else if (isStartTag(xpp, "Period")) { periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs)); } } while (!isEndTag(xpp, "MPD")); - return new MediaPresentationDescription(durationMs, minBufferTimeMs, dynamic, minUpdateTimeMs, - periods); + return new MediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs, + dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, utcTiming, periods); + } + + private UtcTimingElement parseUtcTiming(XmlPullParser xpp) { + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + String value = xpp.getAttributeValue(null, "value"); + return new UtcTimingElement(schemeIdUri, value); } private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs) @@ -429,6 +454,62 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { return parseDurationMs(xpp, name, -1); } + private static long parseDateTime(XmlPullParser xpp, String name, long defaultValue) + throws ParseException { + String value = xpp.getAttributeValue(null, name); + + if (value == null) { + return defaultValue; + } else { + return parseDateTime(value); + } + } + + // VisibleForTesting + static long parseDateTime(String value) throws ParseException { + Matcher matcher = DATE_TIME_PATTERN.matcher(value); + if (!matcher.matches()) { + throw new ParseException("Invalid date/time format: " + value, 0); + } + + int timezoneShift; + if (matcher.group(9) == null) { + // No time zone specified. + timezoneShift = 0; + } else if (matcher.group(9).equalsIgnoreCase("Z")) { + timezoneShift = 0; + } else { + timezoneShift = ((Integer.valueOf(matcher.group(12)) * 60 + + Integer.valueOf(matcher.group(13)))); + if (matcher.group(11).equals("-")) { + timezoneShift *= -1; + } + } + + Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT")); + + dateTime.clear(); + // Note: The month value is 0-based, hence the -1 on group(2) + dateTime.set(Integer.valueOf(matcher.group(1)), + Integer.valueOf(matcher.group(2)) - 1, + Integer.valueOf(matcher.group(3)), + Integer.valueOf(matcher.group(4)), + Integer.valueOf(matcher.group(5)), + Integer.valueOf(matcher.group(6))); + if (!TextUtils.isEmpty(matcher.group(8))) { + final BigDecimal bd = new BigDecimal("0." + matcher.group(8)); + // we care only for milliseconds, so movePointRight(3) + dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue()); + } + + long time = dateTime.getTimeInMillis(); + if (timezoneShift != 0) { + time -= timezoneShift * 60000; + } + + return time; + } + private static long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) { String value = xpp.getAttributeValue(null, name); if (value != null) { diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElement.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElement.java new file mode 100644 index 0000000000..cbcc30de7e --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElement.java @@ -0,0 +1,31 @@ +/* + * 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 UTCTiming element. + */ +public class UtcTimingElement { + + public final String schemeIdUri; + public final String value; + + public UtcTimingElement(String schemeIdUri, String value) { + this.schemeIdUri = schemeIdUri; + this.value = value; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/parser/Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/Extractor.java new file mode 100644 index 0000000000..69db87ae42 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/parser/Extractor.java @@ -0,0 +1,115 @@ +/* + * 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; + +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.upstream.NonBlockingInputStream; + +import java.util.Map; +import java.util.UUID; + +/** + * Facilitates extraction of media samples from a container format. + */ +public interface Extractor { + + /** + * An attempt to read from the input stream returned insufficient data. + */ + public static final int RESULT_NEED_MORE_DATA = 1; + /** + * The end of the input stream was reached. + */ + public static final int RESULT_END_OF_STREAM = 2; + /** + * A media sample was read. + */ + public static final int RESULT_READ_SAMPLE = 4; + /** + * Initialization data was read. The parsed data can be read using {@link #getFormat()} and + * {@link #getPsshInfo}. + */ + public static final int RESULT_READ_INIT = 8; + /** + * A sidx atom was read. The parsed data can be read using {@link #getIndex()}. + */ + public static final int RESULT_READ_INDEX = 16; + /** + * The next thing to be read is a sample, but a {@link SampleHolder} was not supplied. + */ + public static final int RESULT_NEED_SAMPLE_HOLDER = 32; + + /** + * Returns the segment index parsed from the stream. + * + * @return The segment index, or null if a SIDX atom has yet to be parsed. + */ + public SegmentIndex getIndex(); + + /** + * Returns true if the offsets in the index returned by {@link #getIndex()} are relative to the + * first byte following the initialization data, or false if they are absolute (i.e. relative to + * the first byte of the stream). + * + * @return True if the offsets are relative to the first byte following the initialization data. + * False otherwise. + */ + public boolean hasRelativeIndexOffsets(); + + /** + * Returns the format of the samples contained within the media stream. + * + * @return The sample media format, or null if the format has yet to be parsed. + */ + public MediaFormat getFormat(); + + /** + * Returns the pssh information parsed from the stream. + * + * @return The pssh information. May be null if pssh data has yet to be parsed, or if the stream + * does not contain any pssh data. + */ + public Map getPsshInfo(); + + /** + * Consumes data from a {@link NonBlockingInputStream}. + *

+ * The read terminates if the end of the input stream is reached, if an attempt to read from the + * input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate + * both the reason for termination and data that was parsed during the read. + * + * @param inputStream The input stream from which data 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. + */ + public int read(NonBlockingInputStream inputStream, SampleHolder out) throws ParserException; + + /** + * Seeks to a position before or equal to the requested time. + * + * @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 + * fragment run, 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. + */ + public boolean seekTo(long seekTimeUs, boolean allowNoop); + +} 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 936bd565d0..3267d5b409 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 @@ -18,11 +18,13 @@ package com.google.android.exoplayer.parser.mp4; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.parser.Extractor; import com.google.android.exoplayer.parser.SegmentIndex; import com.google.android.exoplayer.parser.mp4.Atom.ContainerAtom; import com.google.android.exoplayer.parser.mp4.Atom.LeafAtom; import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.MimeTypes; import android.annotation.SuppressLint; @@ -47,7 +49,7 @@ import java.util.UUID; *

* This implementation only supports de-muxed (i.e. single track) streams. */ -public final class FragmentedMp4Extractor { +public final class FragmentedMp4Extractor implements Extractor { /** * Flag to work around an issue in some video streams where every frame is marked as a sync frame. @@ -58,32 +60,6 @@ public final class FragmentedMp4Extractor { */ public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1; - /** - * An attempt to read from the input stream returned insufficient data. - */ - public static final int RESULT_NEED_MORE_DATA = 1; - /** - * The end of the input stream was reached. - */ - public static final int RESULT_END_OF_STREAM = 2; - /** - * A media sample was read. - */ - public static final int RESULT_READ_SAMPLE = 4; - /** - * A moov atom was read. The parsed data can be read using {@link #getFormat()} and - * {@link #getPsshInfo}. - */ - public static final int RESULT_READ_INIT = 8; - /** - * A sidx atom was read. The parsed data can be read using {@link #getIndex()}. - */ - public static final int RESULT_READ_INDEX = 16; - /** - * The next thing to be read is a sample, but a {@link SampleHolder} was not supplied. - */ - public static final int RESULT_NEED_SAMPLE_HOLDER = 32; - private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM | RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER; private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; @@ -196,22 +172,13 @@ public final class FragmentedMp4Extractor { } /** - * Returns the segment index parsed from the stream. + * Sideloads track information into the extractor. * - * @return The segment index, or null if a SIDX atom has yet to be parsed. + * @param track The track to sideload. */ - public SegmentIndex getIndex() { - return segmentIndex; - } - - /** - * Returns the pssh information parsed from the stream. - * - * @return The pssh information. May be null if the MOOV atom has yet to be parsed of if it did - * not contain any pssh information. - */ - public Map getPsshInfo() { - return psshData.isEmpty() ? null : psshData; + public void setTrack(Track track) { + this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0); + this.track = track; } /** @@ -228,38 +195,27 @@ public final class FragmentedMp4Extractor { psshData.put(uuid, data); } - /** - * Returns the format of the samples contained within the media stream. - * - * @return The sample media format, or null if a MOOV atom has yet to be parsed. - */ + @Override + public Map getPsshInfo() { + return psshData.isEmpty() ? null : psshData; + } + + @Override + public SegmentIndex getIndex() { + return segmentIndex; + } + + @Override + public boolean hasRelativeIndexOffsets() { + return true; + } + + @Override public MediaFormat getFormat() { return track == null ? null : track.mediaFormat; } - /** - * Sideloads track information into the extractor. - * - * @param track The track to sideload. - */ - public void setTrack(Track track) { - this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0); - this.track = track; - } - - /** - * Consumes data from a {@link NonBlockingInputStream}. - *

- * The read terminates if the end of the input stream is reached, if an attempt to read from the - * input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate - * both the reason for termination and data that was parsed during the read. - * - * @param inputStream The input stream from which data 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. - */ + @Override public int read(NonBlockingInputStream inputStream, SampleHolder out) throws ParserException { try { @@ -286,15 +242,7 @@ public final class FragmentedMp4Extractor { } } - /** - * Seeks to a position before or equal to the requested time. - * - * @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 - * fragment run, 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. - */ + @Override public boolean seekTo(long seekTimeUs, boolean allowNoop) { pendingSeekTimeMs = (int) (seekTimeUs / 1000); if (allowNoop && fragmentRun != null @@ -780,10 +728,10 @@ public final class FragmentedMp4Extractor { LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun); parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out); - TrackEncryptionBox trackEncryptionBox = - track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex]; LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); if (saiz != null) { + TrackEncryptionBox trackEncryptionBox = + track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex]; parseSaiz(trackEncryptionBox, saiz.data, out); } 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 deleted file mode 100644 index 00f24bbad7..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java +++ /dev/null @@ -1,462 +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.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.nio.ByteBuffer; -import java.util.Arrays; -import java.util.concurrent.TimeUnit; - -/** - * Default version of an extractor to facilitate data retrieval from the WebM container format. - * - *

WebM is a subset of the EBML elements defined for Matroska. More information about EBML and - * Matroska is available here. - * More info about WebM is here. - */ -@TargetApi(16) -public final class DefaultWebmExtractor implements WebmExtractor { - - private static final String DOC_TYPE_WEBM = "webm"; - private static final String CODEC_ID_VP9 = "V_VP9"; - private static final int UNKNOWN = -1; - - // Element IDs - private static final int ID_EBML = 0x1A45DFA3; - private static final int ID_EBML_READ_VERSION = 0x42F7; - private static final int ID_DOC_TYPE = 0x4282; - private static final int ID_DOC_TYPE_READ_VERSION = 0x4285; - - private static final int ID_SEGMENT = 0x18538067; - - private static final int ID_INFO = 0x1549A966; - private static final int ID_TIMECODE_SCALE = 0x2AD7B1; - private static final int ID_DURATION = 0x4489; - - private static final int ID_CLUSTER = 0x1F43B675; - private static final int ID_TIME_CODE = 0xE7; - private static final int ID_SIMPLE_BLOCK = 0xA3; - - private static final int ID_TRACKS = 0x1654AE6B; - private static final int ID_TRACK_ENTRY = 0xAE; - private static final int ID_CODEC_ID = 0x86; - private static final int ID_VIDEO = 0xE0; - private static final int ID_PIXEL_WIDTH = 0xB0; - private static final int ID_PIXEL_HEIGHT = 0xBA; - - private static final int ID_CUES = 0x1C53BB6B; - private static final int ID_CUE_POINT = 0xBB; - private static final int ID_CUE_TIME = 0xB3; - private static final int ID_CUE_TRACK_POSITIONS = 0xB7; - private static final int ID_CUE_CLUSTER_POSITION = 0xF1; - - // SimpleBlock Lacing Values - private static final int LACING_NONE = 0; - private static final int LACING_XIPH = 1; - private static final int LACING_FIXED = 2; - private static final int LACING_EBML = 3; - - private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM - | RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER; - - private final EbmlReader reader; - private final byte[] simpleBlockTimecodeAndFlags = new byte[3]; - - private SampleHolder sampleHolder; - private int readResults; - - private long segmentStartOffsetBytes = UNKNOWN; - private long segmentEndOffsetBytes = UNKNOWN; - private long timecodeScale = 1000000L; - private long durationUs = UNKNOWN; - private int pixelWidth = UNKNOWN; - private int pixelHeight = UNKNOWN; - private long cuesSizeBytes = UNKNOWN; - private long clusterTimecodeUs = UNKNOWN; - private long simpleBlockTimecodeUs = UNKNOWN; - private MediaFormat format; - private SegmentIndex cues; - private LongArray cueTimesUs; - private LongArray cueClusterPositions; - - public DefaultWebmExtractor() { - this(new DefaultEbmlReader()); - } - - /* package */ DefaultWebmExtractor(EbmlReader reader) { - this.reader = reader; - this.reader.setEventHandler(new InnerEbmlEventHandler()); - } - - @Override - public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) { - this.sampleHolder = sampleHolder; - this.readResults = 0; - while ((readResults & READ_TERMINATING_RESULTS) == 0) { - int ebmlReadResult = reader.read(inputStream); - if (ebmlReadResult == EbmlReader.READ_RESULT_NEED_MORE_DATA) { - readResults |= WebmExtractor.RESULT_NEED_MORE_DATA; - } else if (ebmlReadResult == EbmlReader.READ_RESULT_END_OF_STREAM) { - readResults |= WebmExtractor.RESULT_END_OF_STREAM; - } - } - this.sampleHolder = null; - return readResults; - } - - @Override - public boolean seekTo(long seekTimeUs, boolean allowNoop) { - if (allowNoop - && cues != null - && clusterTimecodeUs != UNKNOWN - && simpleBlockTimecodeUs != UNKNOWN - && seekTimeUs >= simpleBlockTimecodeUs) { - int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs); - if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) { - return false; - } - } - clusterTimecodeUs = UNKNOWN; - simpleBlockTimecodeUs = UNKNOWN; - reader.reset(); - return true; - } - - @Override - public SegmentIndex getIndex() { - return cues; - } - - @Override - public MediaFormat getFormat() { - return format; - } - - /* package */ int getElementType(int id) { - switch (id) { - case ID_EBML: - case ID_SEGMENT: - case ID_INFO: - case ID_CLUSTER: - case ID_TRACKS: - case ID_TRACK_ENTRY: - case ID_VIDEO: - case ID_CUES: - case ID_CUE_POINT: - case ID_CUE_TRACK_POSITIONS: - return EbmlReader.TYPE_MASTER; - case ID_EBML_READ_VERSION: - case ID_DOC_TYPE_READ_VERSION: - case ID_TIMECODE_SCALE: - case ID_TIME_CODE: - case ID_PIXEL_WIDTH: - case ID_PIXEL_HEIGHT: - case ID_CUE_TIME: - case ID_CUE_CLUSTER_POSITION: - return EbmlReader.TYPE_UNSIGNED_INT; - case ID_DOC_TYPE: - case ID_CODEC_ID: - return EbmlReader.TYPE_STRING; - case ID_SIMPLE_BLOCK: - return EbmlReader.TYPE_BINARY; - case ID_DURATION: - return EbmlReader.TYPE_FLOAT; - default: - return EbmlReader.TYPE_UNKNOWN; - } - } - - /* package */ boolean onMasterElementStart( - int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) { - switch (id) { - case ID_SEGMENT: - if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) { - throw new IllegalStateException("Multiple Segment elements not supported"); - } - segmentStartOffsetBytes = elementOffsetBytes + headerSizeBytes; - segmentEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes; - break; - case ID_CUES: - cuesSizeBytes = headerSizeBytes + contentsSizeBytes; - cueTimesUs = new LongArray(); - cueClusterPositions = new LongArray(); - break; - default: - // pass - } - return true; - } - - /* package */ boolean onMasterElementEnd(int id) { - switch (id) { - case ID_CUES: - buildCues(); - return false; - case ID_VIDEO: - buildFormat(); - return true; - default: - return true; - } - } - - /* package */ boolean onIntegerElement(int id, long value) { - switch (id) { - case ID_EBML_READ_VERSION: - // Validate that EBMLReadVersion is supported. This extractor only supports v1. - if (value != 1) { - throw new IllegalArgumentException("EBMLReadVersion " + value + " not supported"); - } - break; - case ID_DOC_TYPE_READ_VERSION: - // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2. - if (value < 1 || value > 2) { - throw new IllegalArgumentException("DocTypeReadVersion " + value + " not supported"); - } - break; - case ID_TIMECODE_SCALE: - timecodeScale = value; - break; - case ID_PIXEL_WIDTH: - pixelWidth = (int) value; - break; - case ID_PIXEL_HEIGHT: - pixelHeight = (int) value; - break; - case ID_CUE_TIME: - cueTimesUs.add(scaleTimecodeToUs(value)); - break; - case ID_CUE_CLUSTER_POSITION: - cueClusterPositions.add(value); - break; - case ID_TIME_CODE: - clusterTimecodeUs = scaleTimecodeToUs(value); - break; - default: - // pass - } - return true; - } - - /* package */ boolean onFloatElement(int id, double value) { - if (id == ID_DURATION) { - durationUs = scaleTimecodeToUs((long) value); - } - return true; - } - - /* package */ boolean onStringElement(int id, String value) { - switch (id) { - case ID_DOC_TYPE: - // Validate that DocType is supported. This extractor only supports "webm". - if (!DOC_TYPE_WEBM.equals(value)) { - throw new IllegalArgumentException("DocType " + value + " not supported"); - } - break; - case ID_CODEC_ID: - // Validate that CodecID is supported. This extractor only supports "V_VP9". - if (!CODEC_ID_VP9.equals(value)) { - throw new IllegalArgumentException("CodecID " + value + " not supported"); - } - break; - default: - // pass - } - return true; - } - - /* package */ boolean onBinaryElement( - int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes, - NonBlockingInputStream inputStream) { - if (id == ID_SIMPLE_BLOCK) { - // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure - // for info about how data is organized in a SimpleBlock element. - - // If we don't have a sample holder then don't consume the data. - if (sampleHolder == null) { - readResults |= RESULT_NEED_SAMPLE_HOLDER; - return false; - } - - // 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; - sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; - sampleHolder.decodeOnly = invisible; - sampleHolder.timeUs = clusterTimecodeUs + timecodeUs; - sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead()); - break; - case LACING_EBML: - case LACING_FIXED: - case LACING_XIPH: - default: - throw new IllegalStateException("Lacing mode " + lacing + " not supported"); - } - - ByteBuffer outputData = sampleHolder.data; - if (sampleHolder.allowDataBufferReplacement - && (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) { - outputData = ByteBuffer.allocate(sampleHolder.size); - sampleHolder.data = outputData; - } - - if (outputData == null) { - reader.skipBytes(inputStream, sampleHolder.size); - sampleHolder.size = 0; - } else { - reader.readBytes(inputStream, outputData, sampleHolder.size); - } - readResults |= RESULT_READ_SAMPLE; - } - return true; - } - - private long scaleTimecodeToUs(long unscaledTimecode) { - return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale); - } - - /** - * Build a video {@link MediaFormat} containing recently gathered Video information, if needed. - * - *

Replaces the previous {@link #format} only if video width/height have changed. - * {@link #format} is guaranteed to not be null after calling this method. In - * the event that it can't be built, an {@link IllegalStateException} will be thrown. - */ - private void buildFormat() { - if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN - && (format == null || format.width != pixelWidth || format.height != pixelHeight)) { - format = MediaFormat.createVideoFormat( - MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null); - readResults |= RESULT_READ_INIT; - } else if (format == null) { - throw new IllegalStateException("Unable to build format"); - } - } - - /** - * Build a {@link SegmentIndex} containing recently gathered Cues information. - * - *

{@link #cues} is guaranteed to not be null after calling this method. In - * the event that it can't be built, an {@link IllegalStateException} will be thrown. - */ - private void buildCues() { - if (segmentStartOffsetBytes == UNKNOWN) { - throw new IllegalStateException("Segment start/end offsets unknown"); - } else if (durationUs == UNKNOWN) { - throw new IllegalStateException("Duration unknown"); - } else if (cuesSizeBytes == UNKNOWN) { - throw new IllegalStateException("Cues size unknown"); - } else if (cueTimesUs == null || cueClusterPositions == null - || cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) { - throw new IllegalStateException("Invalid/missing cue points"); - } - 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; - readResults |= RESULT_READ_INDEX; - } - - /** - * 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 void onMasterElementStart( - int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) { - DefaultWebmExtractor.this.onMasterElementStart( - id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes); - } - - @Override - public void onMasterElementEnd(int id) { - DefaultWebmExtractor.this.onMasterElementEnd(id); - } - - @Override - public void onIntegerElement(int id, long value) { - DefaultWebmExtractor.this.onIntegerElement(id, value); - } - - @Override - public void onFloatElement(int id, double value) { - DefaultWebmExtractor.this.onFloatElement(id, value); - } - - @Override - public void onStringElement(int id, String value) { - DefaultWebmExtractor.this.onStringElement(id, value); - } - - @Override - public boolean onBinaryElement( - int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes, - NonBlockingInputStream inputStream) { - return DefaultWebmExtractor.this.onBinaryElement( - id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes, inputStream); - } - - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java index e824887476..a0e0b962b3 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 @@ -17,76 +17,460 @@ 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.Extractor; 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.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; /** - * Extractor to facilitate data retrieval from the WebM container format. + * 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. */ -public interface WebmExtractor { +@TargetApi(16) +public final class WebmExtractor implements Extractor { + + 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 static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM + | RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER; + + private final EbmlReader reader; + private final byte[] simpleBlockTimecodeAndFlags = new byte[3]; + + private SampleHolder sampleHolder; + private int readResults; + + 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 WebmExtractor() { + this(new DefaultEbmlReader()); + } + + /* package */ WebmExtractor(EbmlReader reader) { + this.reader = reader; + this.reader.setEventHandler(new InnerEbmlEventHandler()); + } + + @Override + public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) { + this.sampleHolder = sampleHolder; + this.readResults = 0; + while ((readResults & READ_TERMINATING_RESULTS) == 0) { + int ebmlReadResult = reader.read(inputStream); + if (ebmlReadResult == EbmlReader.READ_RESULT_NEED_MORE_DATA) { + readResults |= WebmExtractor.RESULT_NEED_MORE_DATA; + } else if (ebmlReadResult == EbmlReader.READ_RESULT_END_OF_STREAM) { + readResults |= WebmExtractor.RESULT_END_OF_STREAM; + } + } + this.sampleHolder = null; + return readResults; + } + + @Override + public boolean seekTo(long seekTimeUs, boolean allowNoop) { + if (allowNoop + && cues != null + && clusterTimecodeUs != UNKNOWN + && simpleBlockTimecodeUs != UNKNOWN + && seekTimeUs >= simpleBlockTimecodeUs) { + int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs); + if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) { + return false; + } + } + clusterTimecodeUs = UNKNOWN; + simpleBlockTimecodeUs = UNKNOWN; + reader.reset(); + return true; + } + + @Override + public SegmentIndex getIndex() { + return cues; + } + + @Override + public boolean hasRelativeIndexOffsets() { + return false; + } + + @Override + public MediaFormat getFormat() { + return format; + } + + @Override + public Map getPsshInfo() { + // TODO: Parse pssh data from Webm streams. + return null; + } + + /* package */ int getElementType(int id) { + switch (id) { + case ID_EBML: + case ID_SEGMENT: + case ID_INFO: + case ID_CLUSTER: + case ID_TRACKS: + case ID_TRACK_ENTRY: + case ID_VIDEO: + case ID_CUES: + case ID_CUE_POINT: + case ID_CUE_TRACK_POSITIONS: + return EbmlReader.TYPE_MASTER; + case ID_EBML_READ_VERSION: + case ID_DOC_TYPE_READ_VERSION: + case ID_TIMECODE_SCALE: + case ID_TIME_CODE: + case ID_PIXEL_WIDTH: + case ID_PIXEL_HEIGHT: + case ID_CUE_TIME: + case ID_CUE_CLUSTER_POSITION: + return EbmlReader.TYPE_UNSIGNED_INT; + case ID_DOC_TYPE: + case ID_CODEC_ID: + return EbmlReader.TYPE_STRING; + case ID_SIMPLE_BLOCK: + return EbmlReader.TYPE_BINARY; + case ID_DURATION: + return EbmlReader.TYPE_FLOAT; + default: + return EbmlReader.TYPE_UNKNOWN; + } + } + + /* package */ boolean onMasterElementStart( + int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) { + switch (id) { + case ID_SEGMENT: + if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) { + throw new IllegalStateException("Multiple Segment elements not supported"); + } + segmentStartOffsetBytes = elementOffsetBytes + headerSizeBytes; + segmentEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes; + break; + case ID_CUES: + cuesSizeBytes = headerSizeBytes + contentsSizeBytes; + cueTimesUs = new LongArray(); + cueClusterPositions = new LongArray(); + break; + default: + // pass + } + return true; + } + + /* package */ boolean onMasterElementEnd(int id) { + switch (id) { + case ID_CUES: + buildCues(); + return false; + case ID_VIDEO: + buildFormat(); + return true; + default: + return true; + } + } + + /* package */ boolean onIntegerElement(int id, long value) { + switch (id) { + case ID_EBML_READ_VERSION: + // Validate that EBMLReadVersion is supported. This extractor only supports v1. + if (value != 1) { + throw new IllegalArgumentException("EBMLReadVersion " + value + " not supported"); + } + break; + case ID_DOC_TYPE_READ_VERSION: + // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2. + if (value < 1 || value > 2) { + throw new IllegalArgumentException("DocTypeReadVersion " + value + " not supported"); + } + break; + case ID_TIMECODE_SCALE: + timecodeScale = value; + break; + case ID_PIXEL_WIDTH: + pixelWidth = (int) value; + break; + case ID_PIXEL_HEIGHT: + pixelHeight = (int) value; + break; + case ID_CUE_TIME: + cueTimesUs.add(scaleTimecodeToUs(value)); + break; + case ID_CUE_CLUSTER_POSITION: + cueClusterPositions.add(value); + break; + case ID_TIME_CODE: + clusterTimecodeUs = scaleTimecodeToUs(value); + break; + default: + // pass + } + return true; + } + + /* package */ boolean onFloatElement(int id, double value) { + if (id == ID_DURATION) { + durationUs = scaleTimecodeToUs((long) value); + } + return true; + } + + /* package */ boolean onStringElement(int id, String value) { + switch (id) { + case ID_DOC_TYPE: + // Validate that DocType is supported. This extractor only supports "webm". + if (!DOC_TYPE_WEBM.equals(value)) { + throw new IllegalArgumentException("DocType " + value + " not supported"); + } + break; + case ID_CODEC_ID: + // Validate that CodecID is supported. This extractor only supports "V_VP9". + if (!CODEC_ID_VP9.equals(value)) { + throw new IllegalArgumentException("CodecID " + value + " not supported"); + } + break; + default: + // pass + } + return true; + } + + /* package */ boolean onBinaryElement( + int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes, + NonBlockingInputStream inputStream) { + if (id == ID_SIMPLE_BLOCK) { + // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure + // for info about how data is organized in a SimpleBlock element. + + // If we don't have a sample holder then don't consume the data. + if (sampleHolder == null) { + readResults |= RESULT_NEED_SAMPLE_HOLDER; + return false; + } + + // 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; + sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; + sampleHolder.decodeOnly = invisible; + sampleHolder.timeUs = clusterTimecodeUs + timecodeUs; + sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead()); + break; + case LACING_EBML: + case LACING_FIXED: + case LACING_XIPH: + default: + throw new IllegalStateException("Lacing mode " + lacing + " not supported"); + } + + ByteBuffer outputData = sampleHolder.data; + if (sampleHolder.allowDataBufferReplacement + && (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) { + outputData = ByteBuffer.allocate(sampleHolder.size); + sampleHolder.data = outputData; + } + + if (outputData == null) { + reader.skipBytes(inputStream, sampleHolder.size); + sampleHolder.size = 0; + } else { + reader.readBytes(inputStream, outputData, sampleHolder.size); + } + readResults |= RESULT_READ_SAMPLE; + } + return true; + } + + private long scaleTimecodeToUs(long unscaledTimecode) { + return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale); + } /** - * An attempt to read from the input stream returned insufficient data. - */ - public static final int RESULT_NEED_MORE_DATA = 1; - /** - * The end of the input stream was reached. - */ - public static final int RESULT_END_OF_STREAM = 2; - /** - * A media sample was read. - */ - public static final int RESULT_READ_SAMPLE = 4; - /** - * Initialization data was read. The parsed data can be read using {@link #getFormat()}. - */ - public static final int RESULT_READ_INIT = 8; - /** - * A sidx atom was read. The parsed data can be read using {@link #getIndex()}. - */ - public static final int RESULT_READ_INDEX = 16; - /** - * The next thing to be read is a sample, but a {@link SampleHolder} was not supplied. - */ - public static final int RESULT_NEED_SAMPLE_HOLDER = 32; - - /** - * Consumes data from a {@link NonBlockingInputStream}. + * Build a video {@link MediaFormat} containing recently gathered Video information, if needed. * - * @param inputStream The input stream from which data should be read - * @param sampleHolder A {@link SampleHolder} into which the sample should be read - * @return One or more of the {@code RESULT_*} flags defined in this class. + *

Replaces the previous {@link #format} only if video width/height have changed. + * {@link #format} is guaranteed to not be null after calling this method. In + * the event that it can't be built, an {@link IllegalStateException} will be thrown. */ - public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder); + private void buildFormat() { + if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN + && (format == null || format.width != pixelWidth || format.height != pixelHeight)) { + format = MediaFormat.createVideoFormat( + MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null); + readResults |= RESULT_READ_INIT; + } else if (format == null) { + throw new IllegalStateException("Unable to build format"); + } + } /** - * Seeks to a position before or equal to the requested time. + * Build a {@link SegmentIndex} containing recently gathered Cues information. * - * @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 + *

{@link #cues} is guaranteed to not be null after calling this method. In + * the event that it can't be built, an {@link IllegalStateException} will be thrown. */ - public boolean seekTo(long seekTimeUs, boolean allowNoop); + private void buildCues() { + if (segmentStartOffsetBytes == UNKNOWN) { + throw new IllegalStateException("Segment start/end offsets unknown"); + } else if (durationUs == UNKNOWN) { + throw new IllegalStateException("Duration unknown"); + } else if (cuesSizeBytes == UNKNOWN) { + throw new IllegalStateException("Cues size unknown"); + } else if (cueTimesUs == null || cueClusterPositions == null + || cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) { + throw new IllegalStateException("Invalid/missing cue points"); + } + 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; + readResults |= RESULT_READ_INDEX; + } /** - * 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 + * Passes events through to {@link WebmExtractor} as + * callbacks from {@link EbmlReader} are received. */ - public SegmentIndex getIndex(); + private final class InnerEbmlEventHandler implements EbmlEventHandler { - /** - * 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 - */ - public MediaFormat getFormat(); + @Override + public int getElementType(int id) { + return WebmExtractor.this.getElementType(id); + } + + @Override + public void onMasterElementStart( + int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) { + WebmExtractor.this.onMasterElementStart( + id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes); + } + + @Override + public void onMasterElementEnd(int id) { + WebmExtractor.this.onMasterElementEnd(id); + } + + @Override + public void onIntegerElement(int id, long value) { + WebmExtractor.this.onIntegerElement(id, value); + } + + @Override + public void onFloatElement(int id, double value) { + WebmExtractor.this.onFloatElement(id, value); + } + + @Override + public void onStringElement(int id, String value) { + WebmExtractor.this.onStringElement(id, value); + } + + @Override + public boolean onBinaryElement( + int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes, + NonBlockingInputStream inputStream) { + return WebmExtractor.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 d6a70e0364..918ddb4f90 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 @@ -26,7 +26,7 @@ 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.parser.mp4.CodecSpecificDataUtil; +import com.google.android.exoplayer.parser.Extractor; import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer.parser.mp4.Track; import com.google.android.exoplayer.parser.mp4.TrackEncryptionBox; @@ -35,6 +35,7 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.Stre import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; +import com.google.android.exoplayer.util.CodecSpecificDataUtil; import android.net.Uri; import android.util.Base64; @@ -227,7 +228,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { } private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey, - FragmentedMp4Extractor extractor, DataSource dataSource, int chunkIndex, + Extractor extractor, DataSource dataSource, int chunkIndex, boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) { int nextChunkIndex = isLast ? -1 : chunkIndex + 1; long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs; 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 f19a054346..8fb6e66e40 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 @@ -20,8 +20,6 @@ import com.google.android.exoplayer.util.ManifestFetcher; import android.net.Uri; -import org.xmlpull.v1.XmlPullParserException; - import java.io.IOException; import java.io.InputStream; @@ -59,11 +57,7 @@ public final class SmoothStreamingManifestFetcher extends ManifestFetcher 0) { + skippingElementDepth++; + } else if (handleChildInline(tagName)) { parseStartTag(xmlParser); } else { - addChild(newChildParser(this, tagName).parse(xmlParser)); + ElementParser childElementParser = newChildParser(this, tagName); + if (childElementParser == null) { + skippingElementDepth = 1; + } else { + addChild(childElementParser.parse(xmlParser)); + } } } break; case XmlPullParser.TEXT: - if (foundStartTag) { + if (foundStartTag && skippingElementDepth == 0) { parseText(xmlParser); } break; case XmlPullParser.END_TAG: if (foundStartTag) { - tagName = xmlParser.getName(); - parseEndTag(xmlParser); - if (!handleChildInline(tagName)) { - return build(); + if (skippingElementDepth > 0) { + skippingElementDepth--; + } else { + tagName = xmlParser.getName(); + parseEndTag(xmlParser); + if (!handleChildInline(tagName)) { + return build(); + } } } break; @@ -357,6 +372,7 @@ public class SmoothStreamingManifestParser { public static final String KEY_SYSTEM_ID = "SystemID"; + private boolean inProtectionHeader; private UUID uuid; private byte[] initData; @@ -371,16 +387,25 @@ public class SmoothStreamingManifestParser { @Override public void parseStartTag(XmlPullParser parser) { - if (!TAG_PROTECTION_HEADER.equals(parser.getName())) { - return; + if (TAG_PROTECTION_HEADER.equals(parser.getName())) { + inProtectionHeader = true; + String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID); + uuid = UUID.fromString(uuidString); } - String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID); - uuid = UUID.fromString(uuidString); } @Override public void parseText(XmlPullParser parser) { - initData = Base64.decode(parser.getText(), Base64.DEFAULT); + if (inProtectionHeader) { + initData = Base64.decode(parser.getText(), Base64.DEFAULT); + } + } + + @Override + public void parseEndTag(XmlPullParser parser) { + if (TAG_PROTECTION_HEADER.equals(parser.getName())) { + inProtectionHeader = false; + } } @Override @@ -579,9 +604,11 @@ public class SmoothStreamingManifestParser { if (type == StreamElement.TYPE_VIDEO) { maxHeight = parseRequiredInt(parser, KEY_MAX_HEIGHT); maxWidth = parseRequiredInt(parser, KEY_MAX_WIDTH); + fourCC = parseRequiredString(parser, KEY_FOUR_CC); } else { maxHeight = -1; maxWidth = -1; + fourCC = parser.getAttributeValue(null, KEY_FOUR_CC); } if (type == StreamElement.TYPE_AUDIO) { @@ -590,14 +617,12 @@ public class SmoothStreamingManifestParser { bitPerSample = parseRequiredInt(parser, KEY_BITS_PER_SAMPLE); packetSize = parseRequiredInt(parser, KEY_PACKET_SIZE); audioTag = parseRequiredInt(parser, KEY_AUDIO_TAG); - fourCC = parseRequiredString(parser, KEY_FOUR_CC); } else { samplingRate = -1; channels = -1; bitPerSample = -1; packetSize = -1; audioTag = -1; - fourCC = parser.getAttributeValue(null, KEY_FOUR_CC); } value = parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA); diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 1fd213eda1..d6504461b2 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer.text; import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.FormatHolder; +import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; @@ -64,7 +64,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { private final TextRenderer textRenderer; private final SampleSource source; private final SampleHolder sampleHolder; - private final FormatHolder formatHolder; + private final MediaFormatHolder formatHolder; private final SubtitleParser subtitleParser; private int trackIndex; @@ -93,7 +93,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { this.textRenderer = Assertions.checkNotNull(textRenderer); this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, this); - formatHolder = new FormatHolder(); + formatHolder = new MediaFormatHolder(); sampleHolder = new SampleHolder(true); } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java index 379b63fa53..a80df7fb86 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java @@ -41,10 +41,10 @@ public final class CacheDataSource implements DataSource { public interface EventListener { /** - * Invoked when bytes have been read from {@link #cache} since the last invocation. + * Invoked when bytes have been read from the cache. * * @param cacheSizeBytes Current cache size in bytes. - * @param cachedBytesRead Total bytes read from {@link #cache} since last report. + * @param cachedBytesRead Total bytes read from the cache since this method was last invoked. */ void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead); diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java b/library/src/main/java/com/google/android/exoplayer/util/CodecSpecificDataUtil.java similarity index 86% rename from library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java rename to library/src/main/java/com/google/android/exoplayer/util/CodecSpecificDataUtil.java index 851c4925b6..019f7459c0 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/util/CodecSpecificDataUtil.java @@ -13,9 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.parser.mp4; - -import com.google.android.exoplayer.util.Assertions; +package com.google.android.exoplayer.util; import android.annotation.SuppressLint; import android.media.MediaCodecInfo.CodecProfileLevel; @@ -35,6 +33,10 @@ public final class CodecSpecificDataUtil { 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350 }; + private static final int[] AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE = new int[] { + 0, 1, 2, 3, 4, 5, 6, 8 + }; + private static final int SPS_NAL_UNIT_TYPE = 7; private CodecSpecificDataUtil() {} @@ -42,7 +44,7 @@ public final class CodecSpecificDataUtil { /** * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * - * @param audioSpecificConfig + * @param audioSpecificConfig The AudioSpecificConfig to parse. * @return A pair consisting of the sample rate in Hz and the channel count. */ public static Pair parseAudioSpecificConfig(byte[] audioSpecificConfig) { @@ -56,11 +58,27 @@ public final class CodecSpecificDataUtil { return Pair.create(sampleRate, channelCount); } + /** + * Builds a simple AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * + * @param audioObjectType The audio object type. + * @param sampleRateIndex The sample rate index. + * @param channelConfig The channel configuration. + * @return The AudioSpecificConfig. + */ + public static byte[] buildAudioSpecificConfig(int audioObjectType, int sampleRateIndex, + int channelConfig) { + byte[] audioSpecificConfig = new byte[2]; + audioSpecificConfig[0] = (byte) ((audioObjectType << 3) & 0xF8 | (sampleRateIndex >> 1) & 0x07); + audioSpecificConfig[1] = (byte) ((sampleRateIndex << 7) & 0x80 | (channelConfig << 3) & 0x78); + return audioSpecificConfig; + } + /** * Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * * @param sampleRate The sample rate in Hz. - * @param numChannels The number of channels + * @param numChannels The number of channels. * @return The AudioSpecificConfig. */ public static byte[] buildAudioSpecificConfig(int sampleRate, int numChannels) { @@ -70,10 +88,16 @@ public final class CodecSpecificDataUtil { sampleRateIndex = i; } } + int channelConfig = -1; + for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE.length; ++i) { + if (numChannels == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) { + channelConfig = i; + } + } // The full specification for AudioSpecificConfig is stated in ISO 14496-3 Section 1.6.2.1 byte[] csd = new byte[2]; csd[0] = (byte) ((2 /* AAC LC */ << 3) | (sampleRateIndex >> 1)); - csd[1] = (byte) (((sampleRateIndex & 0x1) << 7) | (numChannels << 3)); + csd[1] = (byte) (((sampleRateIndex & 0x1) << 7) | (channelConfig << 3)); return csd; }