diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java index f8ceebc600..6479b28a7e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java @@ -45,7 +45,7 @@ public class DemoUtil { public static final String CONTENT_ID_EXTRA = "content_id"; public static final int TYPE_DASH_VOD = 0; - public static final int TYPE_SS_VOD = 1; + public static final int TYPE_SS = 1; public static final int TYPE_OTHER = 2; public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false; 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 39b21b62bf..93d08af4cc 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 @@ -56,7 +56,7 @@ package com.google.android.exoplayer.demo; false), new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed", "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism", - DemoUtil.TYPE_SS_VOD, false, false), + DemoUtil.TYPE_SS, false, false), new Sample("Dizzy (Misc)", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false, false), }; @@ -92,10 +92,10 @@ package com.google.android.exoplayer.demo; public static final Sample[] SMOOTHSTREAMING = new Sample[] { new Sample("Super speed", "uid:ss:superspeed", "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism", - DemoUtil.TYPE_SS_VOD, false, true), + DemoUtil.TYPE_SS, false, true), new Sample("Super speed (PlayReady)", "uid:ss:pr:superspeed", "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism", - DemoUtil.TYPE_SS_VOD, true, true), + DemoUtil.TYPE_SS, true, true), }; public static final Sample[] WIDEVINE_GTS = new Sample[] { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 48ed2f5ded..9966124ced 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -167,7 +167,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba private RendererBuilder getRendererBuilder() { String userAgent = DemoUtil.getUserAgent(this); switch (contentType) { - case DemoUtil.TYPE_SS_VOD: + case DemoUtil.TYPE_SS: return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), contentId, new SmoothStreamingTestMediaDrmCallback(), debugTextView); case DemoUtil.TYPE_DASH_VOD: diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index a5cb7093e4..940691b48c 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -65,6 +65,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int AUDIO_BUFFER_SEGMENTS = 60; private static final int TTML_BUFFER_SEGMENTS = 2; + private static final int LIVE_EDGE_LATENCY_MS = 30000; private final String userAgent; private final String url; @@ -74,6 +75,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, private DemoPlayer player; private RendererBuilderCallback callback; + private ManifestFetcher manifestFetcher; public SmoothStreamingRendererBuilder(String userAgent, String url, String contentId, MediaDrmCallback drmCallback, TextView debugTextView) { @@ -89,8 +91,8 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, this.player = player; this.callback = callback; SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); - ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, contentId, url + "/Manifest"); + manifestFetcher = new ManifestFetcher(parser, contentId, + url + "/Manifest"); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @@ -154,9 +156,9 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, // Build the video renderer. DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); - ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifest, + ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher, videoStreamElementIndex, videoTrackIndices, videoDataSource, - new AdaptiveEvaluator(bandwidthMeter)); + new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_VIDEO); @@ -181,8 +183,9 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, for (int i = 0; i < manifest.streamElements.length; i++) { if (manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) { audioTrackNames[audioStreamElementCount] = manifest.streamElements[i].name; - audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(manifest, - i, new int[] {0}, audioDataSource, audioFormatEvaluator); + audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource( + manifestFetcher, i, new int[] {0}, audioDataSource, audioFormatEvaluator, + LIVE_EDGE_LATENCY_MS); audioStreamElementCount++; } } @@ -211,8 +214,8 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, for (int i = 0; i < manifest.streamElements.length; i++) { if (manifest.streamElements[i].type == StreamElement.TYPE_TEXT) { textTrackNames[textStreamElementCount] = manifest.streamElements[i].language; - textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(manifest, - i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator); + textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(manifestFetcher, + i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator, LIVE_EDGE_LATENCY_MS); textStreamElementCount++; } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java index f936b19219..eb80499ebd 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java @@ -54,6 +54,7 @@ import java.util.ArrayList; private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int AUDIO_BUFFER_SEGMENTS = 60; + private static final int LIVE_EDGE_LATENCY_MS = 30000; private final SimplePlayerActivity playerActivity; private final String userAgent; @@ -61,6 +62,7 @@ import java.util.ArrayList; private final String contentId; private RendererBuilderCallback callback; + private ManifestFetcher manifestFetcher; public SmoothStreamingRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url, String contentId) { @@ -74,8 +76,8 @@ import java.util.ArrayList; public void buildRenderers(RendererBuilderCallback callback) { this.callback = callback; SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); - ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, contentId, url + "/Manifest"); + manifestFetcher = new ManifestFetcher(parser, contentId, + url + "/Manifest"); manifestFetcher.singleLoad(playerActivity.getMainLooper(), this); } @@ -120,8 +122,9 @@ import java.util.ArrayList; // Build the video renderer. DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); - ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifest, videoStreamElementIndex, - videoTrackIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter)); + ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher, + videoStreamElementIndex, videoTrackIndices, videoDataSource, + new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, @@ -129,8 +132,9 @@ import java.util.ArrayList; // Build the audio renderer. DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); - ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifest, audioStreamElementIndex, - new int[] {0}, audioDataSource, new FormatEvaluator.FixedEvaluator()); + ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher, + audioStreamElementIndex, new int[] {0}, audioDataSource, + new FormatEvaluator.FixedEvaluator(), LIVE_EDGE_LATENCY_MS); SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer( diff --git a/library/src/main/java/com/google/android/exoplayer/BehindLiveWindowException.java b/library/src/main/java/com/google/android/exoplayer/BehindLiveWindowException.java new file mode 100644 index 0000000000..074a1de01a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/BehindLiveWindowException.java @@ -0,0 +1,33 @@ +/* + * 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; + +import java.io.IOException; + +/** + * Thrown when a live playback falls behind the available media window. + */ +public class BehindLiveWindowException extends IOException { + + public BehindLiveWindowException() { + super(); + } + + public BehindLiveWindowException(String message) { + super(message); + } + +} 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 74ee50925e..2b676e6b52 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.smoothstreaming; +import com.google.android.exoplayer.BehindLiveWindowException; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.chunk.Chunk; @@ -36,8 +37,10 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.Trac import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.util.CodecSpecificDataUtil; +import com.google.android.exoplayer.util.ManifestFetcher; import android.net.Uri; +import android.os.SystemClock; import android.util.Base64; import android.util.SparseArray; @@ -51,13 +54,16 @@ import java.util.List; */ public class SmoothStreamingChunkSource implements ChunkSource { + private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000; private static final int INITIALIZATION_VECTOR_SIZE = 8; - private final StreamElement streamElement; + private final ManifestFetcher manifestFetcher; + private final int streamElementIndex; private final TrackInfo trackInfo; private final DataSource dataSource; private final FormatEvaluator formatEvaluator; private final Evaluation evaluation; + private final long liveEdgeLatencyUs; private final int maxWidth; private final int maxHeight; @@ -65,7 +71,42 @@ public class SmoothStreamingChunkSource implements ChunkSource { private final SparseArray extractors; private final SmoothStreamingFormat[] formats; + private SmoothStreamingManifest currentManifest; + private int currentManifestChunkOffset; + private boolean finishedCurrentManifest; + + private IOException fatalError; + /** + * Constructor to use for live streaming. + *

+ * May also be used for fixed duration content, in which case the call is equivalent to calling + * the other constructor, passing {@code manifestFetcher.getManifest()} is the first argument. + * + * @param manifestFetcher A fetcher for the manifest, which must have already successfully + * completed an initial load. + * @param streamElementIndex The index of the stream element in the manifest to be provided by + * the source. + * @param trackIndices The indices of the tracks within the stream element to be considered by + * the source. May be null if all tracks within the element should be considered. + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param formatEvaluator Selects from the available formats. + * @param liveEdgeLatencyMs For live streams, the number of milliseconds that the playback should + * lag behind the "live edge" (i.e. the end of the most recently defined media in the + * manifest). Choosing a small value will minimize latency introduced by the player, however + * note that the value sets an upper bound on the length of media that the player can buffer. + * Hence a small value may increase the probability of rebuffering and playback failures. + */ + public SmoothStreamingChunkSource(ManifestFetcher manifestFetcher, + int streamElementIndex, int[] trackIndices, DataSource dataSource, + FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) { + this(manifestFetcher, manifestFetcher.getManifest(), streamElementIndex, trackIndices, + dataSource, formatEvaluator, liveEdgeLatencyMs); + } + + /** + * Constructor to use for fixed duration content. + * * @param manifest The manifest parsed from {@code baseUrl + "/Manifest"}. * @param streamElementIndex The index of the stream element in the manifest to be provided by * the source. @@ -76,14 +117,25 @@ public class SmoothStreamingChunkSource implements ChunkSource { */ public SmoothStreamingChunkSource(SmoothStreamingManifest manifest, int streamElementIndex, int[] trackIndices, DataSource dataSource, FormatEvaluator formatEvaluator) { - this.streamElement = manifest.streamElements[streamElementIndex]; - this.trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, manifest.durationUs); + this(null, manifest, streamElementIndex, trackIndices, dataSource, formatEvaluator, 0); + } + + private SmoothStreamingChunkSource(ManifestFetcher manifestFetcher, + SmoothStreamingManifest initialManifest, int streamElementIndex, int[] trackIndices, + DataSource dataSource, FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) { + this.manifestFetcher = manifestFetcher; + this.streamElementIndex = streamElementIndex; + this.currentManifest = initialManifest; this.dataSource = dataSource; this.formatEvaluator = formatEvaluator; - this.evaluation = new Evaluation(); + this.liveEdgeLatencyUs = liveEdgeLatencyMs * 1000; + + StreamElement streamElement = getElement(initialManifest); + trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, initialManifest.durationUs); + evaluation = new Evaluation(); TrackEncryptionBox[] trackEncryptionBoxes = null; - ProtectionElement protectionElement = manifest.protectionElement; + ProtectionElement protectionElement = initialManifest.protectionElement; if (protectionElement != null) { byte[] keyId = getKeyId(protectionElement.data); trackEncryptionBoxes = new TrackEncryptionBox[1]; @@ -135,22 +187,52 @@ public class SmoothStreamingChunkSource implements ChunkSource { @Override public void enable() { - // Do nothing. + fatalError = null; + if (manifestFetcher != null) { + manifestFetcher.enable(); + } } @Override public void disable(List queue) { - // Do nothing. + if (manifestFetcher != null) { + manifestFetcher.disable(); + } } @Override public void continueBuffering(long playbackPositionUs) { - // Do nothing + if (manifestFetcher == null || !currentManifest.isLive || fatalError != null) { + return; + } + + SmoothStreamingManifest newManifest = manifestFetcher.getManifest(); + if (currentManifest != newManifest && newManifest != null) { + StreamElement currentElement = getElement(currentManifest); + StreamElement newElement = getElement(newManifest); + if (newElement.chunkCount == 0) { + currentManifestChunkOffset += currentElement.chunkCount; + } else if (currentElement.chunkCount > 0) { + currentManifestChunkOffset += currentElement.getChunkIndex(newElement.getStartTimeUs(0)); + } + currentManifest = newManifest; + finishedCurrentManifest = false; + } + + if (finishedCurrentManifest && (SystemClock.elapsedRealtime() + > manifestFetcher.getManifestLoadTimestamp() + MINIMUM_MANIFEST_REFRESH_PERIOD_MS)) { + manifestFetcher.requestRefresh(); + } } @Override public final void getChunkOperation(List queue, long seekPositionUs, long playbackPositionUs, ChunkOperationHolder out) { + if (fatalError != null) { + out.chunk = null; + return; + } + evaluation.queueSize = queue.size(); formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation); SmoothStreamingFormat selectedFormat = (SmoothStreamingFormat) evaluation.format; @@ -166,30 +248,63 @@ public class SmoothStreamingChunkSource implements ChunkSource { return; } - int nextChunkIndex; - if (queue.isEmpty()) { - nextChunkIndex = streamElement.getChunkIndex(seekPositionUs); - } else { - nextChunkIndex = queue.get(out.queueSize - 1).nextChunkIndex; - } + // In all cases where we return before instantiating a new chunk at the bottom of this method, + // we want out.chunk to be null. + out.chunk = null; - if (nextChunkIndex == -1) { - out.chunk = null; + StreamElement streamElement = getElement(currentManifest); + if (streamElement.chunkCount == 0) { + // The manifest is currently empty for this stream. + finishedCurrentManifest = true; return; } - boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1; - Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, nextChunkIndex); + int chunkIndex; + if (queue.isEmpty()) { + if (currentManifest.isLive) { + seekPositionUs = getLiveSeekPosition(); + } + chunkIndex = streamElement.getChunkIndex(seekPositionUs); + } else { + chunkIndex = queue.get(out.queueSize - 1).nextChunkIndex - currentManifestChunkOffset; + } + + if (currentManifest.isLive) { + if (chunkIndex < 0) { + // This is before the first chunk in the current manifest. + fatalError = new BehindLiveWindowException(); + return; + } else if (chunkIndex >= streamElement.chunkCount) { + // This is beyond the last chunk in the current manifest. + finishedCurrentManifest = true; + return; + } else if (chunkIndex == streamElement.chunkCount - 1) { + // This is the last chunk in the current manifest. Mark the manifest as being finished, + // but continue to return the final chunk. + finishedCurrentManifest = true; + } + } else if (chunkIndex == -1) { + // We've reached the end of the stream. + return; + } + + boolean isLastChunk = !currentManifest.isLive && chunkIndex == streamElement.chunkCount - 1; + long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); + long nextChunkStartTimeUs = isLastChunk ? -1 + : chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); + int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset; + + Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex); Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, - extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, nextChunkIndex, - isLastChunk, streamElement.getStartTimeUs(nextChunkIndex), - isLastChunk ? -1 : streamElement.getStartTimeUs(nextChunkIndex + 1), 0); + extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, currentAbsoluteChunkIndex, + isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0); out.chunk = mediaChunk; } @Override public IOException getError() { - return null; + return fatalError != null ? fatalError + : (manifestFetcher != null ? manifestFetcher.getError() : null); } @Override @@ -197,6 +312,30 @@ public class SmoothStreamingChunkSource implements ChunkSource { // Do nothing. } + /** + * For live playbacks, determines the seek position that snaps playback to be + * {@link #liveEdgeLatencyUs} behind the live edge of the current manifest + * + * @return The seek position in microseconds. + */ + private long getLiveSeekPosition() { + long liveEdgeTimestampUs = Long.MIN_VALUE; + for (int i = 0; i < currentManifest.streamElements.length; i++) { + StreamElement streamElement = currentManifest.streamElements[i]; + if (streamElement.chunkCount > 0) { + long elementLiveEdgeTimestampUs = + streamElement.getStartTimeUs(streamElement.chunkCount - 1) + + streamElement.getChunkDurationUs(streamElement.chunkCount - 1); + liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, elementLiveEdgeTimestampUs); + } + } + return liveEdgeTimestampUs - liveEdgeLatencyUs; + } + + private StreamElement getElement(SmoothStreamingManifest manifest) { + return manifest.streamElements[streamElementIndex]; + } + private static MediaFormat getMediaFormat(StreamElement streamElement, int trackIndex) { TrackElement trackElement = streamElement.tracks[trackIndex]; String mimeType = trackElement.mimeType;