From 4efc0abde92a07c91d54c55ac32f821aec004eb7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 3 Dec 2014 18:45:13 +0000 Subject: [PATCH] Implement DASH Live. Note: This adds support for the majority of DASH live streams, however we do not yet correctly support live streams that rely on UtcTimingElements in their manifests. Issue: #52 --- .../android/exoplayer/demo/DemoUtil.java | 2 +- .../android/exoplayer/demo/Samples.java | 24 +- .../demo/full/FullPlayerActivity.java | 6 +- ...rBuilder.java => DashRendererBuilder.java} | 193 ++++++---- .../SmoothStreamingRendererBuilder.java | 9 +- ...rBuilder.java => DashRendererBuilder.java} | 77 ++-- .../demo/simple/SimplePlayerActivity.java | 12 +- .../SmoothStreamingRendererBuilder.java | 6 +- .../chunk/MultiTrackChunkSource.java | 10 + .../exoplayer/dash/DashChunkSource.java | 346 +++++++++++++++--- .../android/exoplayer/dash/mpd/Period.java | 17 + .../exoplayer/util/ManifestFetcher.java | 77 +++- .../android/exoplayer/util/PlayerControl.java | 11 +- .../google/android/exoplayer/util/Util.java | 18 + 14 files changed, 609 insertions(+), 199 deletions(-) rename demo/src/main/java/com/google/android/exoplayer/demo/full/player/{DashVodRendererBuilder.java => DashRendererBuilder.java} (57%) rename demo/src/main/java/com/google/android/exoplayer/demo/simple/{DashVodRendererBuilder.java => DashRendererBuilder.java} (62%) 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 880ecc3286..a55e2c2cb0 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 @@ -47,7 +47,7 @@ public class DemoUtil { public static final String CONTENT_TYPE_EXTRA = "content_type"; public static final String CONTENT_ID_EXTRA = "content_id"; - public static final int TYPE_DASH_VOD = 0; + public static final int TYPE_DASH = 0; public static final int TYPE_SS = 1; public static final int TYPE_OTHER = 2; 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 27d71a7d55..deea767d07 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 @@ -46,13 +46,13 @@ package com.google.android.exoplayer.demo; "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&" + "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D." - + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH_VOD, false, + + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false, false), new Sample("Google Play (DASH)", "3aa39fa2cc27967f", "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&" + "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A." - + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH_VOD, false, + + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false, false), new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed", "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism", @@ -66,13 +66,13 @@ package com.google.android.exoplayer.demo; "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&" + "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D." - + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH_VOD, false, + + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false, true), new Sample("Google Play", "3aa39fa2cc27967f", "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&" + "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A." - + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH_VOD, false, + + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false, true), }; @@ -81,12 +81,12 @@ package com.google.android.exoplayer.demo; "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" + "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&" + "expire=19000000000&signature=A3EC7EE53ABE601B357F7CAB8B54AD0702CA85A7." - + "446E9C38E47E3EDAF39E0163C390FF83A7944918&key=ik0", DemoUtil.TYPE_DASH_VOD, false, true), + + "446E9C38E47E3EDAF39E0163C390FF83A7944918&key=ik0", DemoUtil.TYPE_DASH, false, true), new Sample("Google Play", "3aa39fa2cc27967f", "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" + "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&" + "expire=19000000000&signature=B752B262C6D7262EC4E4EB67901E5D8F7058A81D." - + "C0358CE1E335417D9A8D88FF192F0D5D8F6DA1B6&key=ik0", DemoUtil.TYPE_DASH_VOD, false, true), + + "C0358CE1E335417D9A8D88FF192F0D5D8F6DA1B6&key=ik0", DemoUtil.TYPE_DASH, false, true), }; public static final Sample[] SMOOTHSTREAMING = new Sample[] { @@ -103,32 +103,32 @@ package com.google.android.exoplayer.demo; "http://www.youtube.com/api/manifest/dash/id/d286538032258a1c/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=41EA40A027A125A16292E0A5E3277A3B5FA9B938." - + "0BB075C396FFDDC97E526E8F77DC26FF9667D0D6&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + + "0BB075C396FFDDC97E526E8F77DC26FF9667D0D6&key=ik0", DemoUtil.TYPE_DASH, true, true), new Sample("WV: HDCP not required", "48fcc369939ac96c", "http://www.youtube.com/api/manifest/dash/id/48fcc369939ac96c/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=315911BDCEED0FB0C763455BDCC97449DAAFA9E8." - + "5B41E2EB411F797097A359D6671D2CDE26272373&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + + "5B41E2EB411F797097A359D6671D2CDE26272373&key=ik0", DemoUtil.TYPE_DASH, true, true), new Sample("WV: HDCP required", "e06c39f1151da3df", "http://www.youtube.com/api/manifest/dash/id/e06c39f1151da3df/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=A47A1E13E7243BD567601A75F79B34644D0DC592." - + "B09589A34FA23527EFC1552907754BB8033870BD&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + + "B09589A34FA23527EFC1552907754BB8033870BD&key=ik0", DemoUtil.TYPE_DASH, true, true), new Sample("WV: Secure video path required", "0894c7c8719b28a0", "http://www.youtube.com/api/manifest/dash/id/0894c7c8719b28a0/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=2847EE498970F6B45176766CD2802FEB4D4CB7B2." - + "A1CA51EC40A1C1039BA800C41500DD448C03EEDA&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + + "A1CA51EC40A1C1039BA800C41500DD448C03EEDA&key=ik0", DemoUtil.TYPE_DASH, true, true), new Sample("WV: HDCP + secure video path required", "efd045b1eb61888a", "http://www.youtube.com/api/manifest/dash/id/efd045b1eb61888a/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=61611F115EEEC7BADE5536827343FFFE2D83D14F." - + "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + + "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH, true, true), new Sample("WV: 30s license duration", "f9a34cab7b05881a", "http://www.youtube.com/api/manifest/dash/id/f9a34cab7b05881a/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6." - + "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true), + + "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH, true, true), }; public static final Sample[] MISC = 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 c1fe79eb0f..423af3d40e 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 @@ -19,7 +19,7 @@ import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.VideoSurfaceView; import com.google.android.exoplayer.demo.DemoUtil; import com.google.android.exoplayer.demo.R; -import com.google.android.exoplayer.demo.full.player.DashVodRendererBuilder; +import com.google.android.exoplayer.demo.full.player.DashRendererBuilder; import com.google.android.exoplayer.demo.full.player.DefaultRendererBuilder; import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; @@ -172,8 +172,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba case DemoUtil.TYPE_SS: return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), contentId, new SmoothStreamingTestMediaDrmCallback(), debugTextView); - case DemoUtil.TYPE_DASH_VOD: - return new DashVodRendererBuilder(userAgent, contentUri.toString(), contentId, + case DemoUtil.TYPE_DASH: + return new DashRendererBuilder(userAgent, contentUri.toString(), contentId, new WidevineTestMediaDrmCallback(contentId), debugTextView); default: return new DefaultRendererBuilder(this, contentUri, debugTextView); 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/DashRendererBuilder.java similarity index 57% rename from demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java rename to demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java index 9ce6613fa2..f998d6e30f 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/DashRendererBuilder.java @@ -40,6 +40,8 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderC import com.google.android.exoplayer.drm.DrmSessionManager; import com.google.android.exoplayer.drm.MediaDrmCallback; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.text.TextTrackRenderer; +import com.google.android.exoplayer.text.webvtt.WebvttParser; import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; @@ -58,16 +60,19 @@ import android.widget.TextView; import java.io.IOException; import java.util.ArrayList; +import java.util.List; /** - * A {@link RendererBuilder} for DASH VOD. + * A {@link RendererBuilder} for DASH. */ -public class DashVodRendererBuilder implements RendererBuilder, +public class DashRendererBuilder implements RendererBuilder, ManifestCallback { 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 TEXT_BUFFER_SEGMENTS = 2; + private static final int LIVE_EDGE_LATENCY_MS = 30000; private static final int SECURITY_LEVEL_UNKNOWN = -1; private static final int SECURITY_LEVEL_1 = 1; @@ -81,8 +86,9 @@ public class DashVodRendererBuilder implements RendererBuilder, private DemoPlayer player; private RendererBuilderCallback callback; + private ManifestFetcher manifestFetcher; - public DashVodRendererBuilder(String userAgent, String url, String contentId, + public DashRendererBuilder(String userAgent, String url, String contentId, MediaDrmCallback drmCallback, TextView debugTextView) { this.userAgent = userAgent; this.url = url; @@ -96,8 +102,8 @@ public class DashVodRendererBuilder implements RendererBuilder, this.player = player; this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, contentId, url, userAgent); + manifestFetcher = new ManifestFetcher(parser, contentId, url, + userAgent); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @@ -108,38 +114,17 @@ public class DashVodRendererBuilder implements RendererBuilder, @Override public void onManifest(String contentId, MediaPresentationDescription manifest) { + Period period = manifest.periods.get(0); Handler mainHandler = player.getMainHandler(); LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); - // Obtain Representations for playback. - int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); - ArrayList audioRepresentationsList = new ArrayList(); - ArrayList videoRepresentationsList = new ArrayList(); - Period period = manifest.periods.get(0); - boolean hasContentProtection = false; - for (int i = 0; i < period.adaptationSets.size(); i++) { - AdaptationSet adaptationSet = period.adaptationSets.get(i); - hasContentProtection |= adaptationSet.hasContentProtection(); - int adaptationSetType = adaptationSet.type; - for (int j = 0; j < adaptationSet.representations.size(); j++) { - Representation representation = adaptationSet.representations.get(j); - if (adaptationSetType == AdaptationSet.TYPE_AUDIO) { - audioRepresentationsList.add(representation); - } else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { - Format format = representation.format; - if (format.width * format.height <= maxDecodableFrameSize) { - videoRepresentationsList.add(representation); - } else { - // The device isn't capable of playing this stream. - } - } - } - } - Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()]; - videoRepresentationsList.toArray(videoRepresentations); + int videoAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_VIDEO); + AdaptationSet videoAdaptationSet = period.adaptationSets.get(videoAdaptationSetIndex); // Check drm support if necessary. + boolean hasContentProtection = videoAdaptationSet.hasContentProtection(); + boolean filterHdContent = false; DrmSessionManager drmSessionManager = null; if (hasContentProtection) { if (Util.SDK_INT < 18) { @@ -151,55 +136,81 @@ public class DashVodRendererBuilder implements RendererBuilder, Pair drmSessionManagerData = V18Compat.getDrmSessionManagerData(player, drmCallback); drmSessionManager = drmSessionManagerData.first; - if (!drmSessionManagerData.second) { - // HD streams require L1 security. - videoRepresentations = getSdRepresentations(videoRepresentations); - } + // HD streams require L1 security. + filterHdContent = !drmSessionManagerData.second; } catch (Exception e) { callback.onRenderersError(e); return; } } - // Build the video renderer. - DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); - ChunkSource videoChunkSource; - String mimeType = videoRepresentations[0].format.mimeType; - 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); + // Determine which video representations we should use for playback. + int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); + List videoRepresentations = videoAdaptationSet.representations; + ArrayList videoRepresentationIndexList = new ArrayList(); + for (int i = 0; i < videoRepresentations.size(); i++) { + Format format = videoRepresentations.get(i).format; + if (filterHdContent && (format.width >= 1280 || format.height >= 720)) { + // Filtering HD content + } else if (format.width * format.height > maxDecodableFrameSize) { + // Filtering stream that device cannot play + } else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4) + && !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) { + // Filtering unsupported mime type + } else { + videoRepresentationIndexList.add(i); + } + } + + // Build the video renderer. + final MediaCodecVideoTrackRenderer videoRenderer; + final TrackRenderer debugRenderer; + if (videoRepresentationIndexList.isEmpty()) { + videoRenderer = null; + debugRenderer = null; + } else { + int[] videoRepresentationIndices = Util.toArray(videoRepresentationIndexList); + DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); + ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex, + videoRepresentationIndices, videoDataSource, 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); + videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50); + debugRenderer = debugTextView != null + ? new DebugTrackRenderer(debugTextView, videoRenderer, videoSampleSource) : null; + } + + // Build the audio chunk sources. + int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO); + AdaptationSet audioAdaptationSet = period.adaptationSets.get(audioAdaptationSetIndex); + DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); + FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); + List audioChunkSourceList = new ArrayList(); + List audioTrackNameList = new ArrayList(); + List audioRepresentations = audioAdaptationSet.representations; + for (int i = 0; i < audioRepresentations.size(); i++) { + Format format = audioRepresentations.get(i).format; + audioTrackNameList.add(format.id + " (" + format.numChannels + "ch, " + + format.audioSamplingRate + "Hz)"); + audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex, + new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS)); } - ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, - VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, - DemoPlayer.TYPE_VIDEO); - MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, - drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, - mainHandler, player, 50); // Build the audio renderer. final String[] audioTrackNames; final MultiTrackChunkSource audioChunkSource; final TrackRenderer audioRenderer; - if (audioRepresentationsList.isEmpty()) { + if (audioChunkSourceList.isEmpty()) { audioTrackNames = null; audioChunkSource = null; audioRenderer = null; } else { - DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); - audioTrackNames = new String[audioRepresentationsList.size()]; - ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()]; - FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); - for (int i = 0; i < audioRepresentationsList.size(); i++) { - Representation representation = audioRepresentationsList.get(i); - Format format = representation.format; - audioTrackNames[i] = format.id + " (" + format.numChannels + "ch, " + - format.audioSamplingRate + "Hz)"; - audioChunkSources[i] = new DashChunkSource(audioDataSource, - audioEvaluator, representation); - } - audioChunkSource = new MultiTrackChunkSource(audioChunkSources); + audioTrackNames = new String[audioTrackNameList.size()]; + audioTrackNameList.toArray(audioTrackNames); + audioChunkSource = new MultiTrackChunkSource(audioChunkSourceList); SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_AUDIO); @@ -207,37 +218,61 @@ public class DashVodRendererBuilder implements RendererBuilder, mainHandler, player); } - // Build the debug renderer. - TrackRenderer debugRenderer = debugTextView != null - ? new DebugTrackRenderer(debugTextView, videoRenderer, videoSampleSource) : null; + // Build the text chunk sources. + DataSource textDataSource = new UriDataSource(userAgent, bandwidthMeter); + FormatEvaluator textEvaluator = new FormatEvaluator.FixedEvaluator(); + List textChunkSourceList = new ArrayList(); + List textTrackNameList = new ArrayList(); + for (int i = 0; i < period.adaptationSets.size(); i++) { + AdaptationSet adaptationSet = period.adaptationSets.get(i); + if (adaptationSet.type == AdaptationSet.TYPE_TEXT) { + List representations = adaptationSet.representations; + for (int j = 0; j < representations.size(); j++) { + Representation representation = representations.get(j); + textTrackNameList.add(representation.format.id); + textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j}, + textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS)); + } + } + } + + // Build the text renderers + final String[] textTrackNames; + final MultiTrackChunkSource textChunkSource; + final TrackRenderer textRenderer; + if (textChunkSourceList.isEmpty()) { + textTrackNames = null; + textChunkSource = null; + textRenderer = null; + } else { + textTrackNames = new String[textTrackNameList.size()]; + textTrackNameList.toArray(textTrackNames); + textChunkSource = new MultiTrackChunkSource(textChunkSourceList); + SampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, + TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, + DemoPlayer.TYPE_TEXT); + textRenderer = new TextTrackRenderer(textSampleSource, new WebvttParser(), player, + mainHandler.getLooper()); + } // Invoke the callback. String[][] trackNames = new String[DemoPlayer.RENDERER_COUNT][]; trackNames[DemoPlayer.TYPE_AUDIO] = audioTrackNames; + trackNames[DemoPlayer.TYPE_TEXT] = textTrackNames; MultiTrackChunkSource[] multiTrackChunkSources = new MultiTrackChunkSource[DemoPlayer.RENDERER_COUNT]; multiTrackChunkSources[DemoPlayer.TYPE_AUDIO] = audioChunkSource; + multiTrackChunkSources[DemoPlayer.TYPE_TEXT] = textChunkSource; TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; + renderers[DemoPlayer.TYPE_TEXT] = textRenderer; renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; callback.onRenderers(trackNames, multiTrackChunkSources, renderers); } - private Representation[] getSdRepresentations(Representation[] representations) { - ArrayList sdRepresentations = new ArrayList(); - for (int i = 0; i < representations.length; i++) { - if (representations[i].format.height < 720 && representations[i].format.width < 1280) { - sdRepresentations.add(representations[i]); - } - } - Representation[] sdRepresentationArray = new Representation[sdRepresentations.size()]; - sdRepresentations.toArray(sdRepresentationArray); - return sdRepresentationArray; - } - @TargetApi(18) private static class V18Compat { 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 2fb473239f..7d88519b45 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 @@ -64,7 +64,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, 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 TTML_BUFFER_SEGMENTS = 2; + private static final int TEXT_BUFFER_SEGMENTS = 2; private static final int LIVE_EDGE_LATENCY_MS = 30000; private final String userAgent; @@ -149,10 +149,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } } } - int[] videoTrackIndices = new int[videoTrackIndexList.size()]; - for (int i = 0; i < videoTrackIndexList.size(); i++) { - videoTrackIndices[i] = videoTrackIndexList.get(i); - } + int[] videoTrackIndices = Util.toArray(videoTrackIndexList); // Build the video renderer. DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); @@ -221,7 +218,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } textChunkSource = new MultiTrackChunkSource(textChunkSources); ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl, - TTML_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, + TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_TEXT); textRenderer = new TextTrackRenderer(ttmlSampleSource, new TtmlParser(), player, mainHandler.getLooper()); 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/DashRendererBuilder.java similarity index 62% rename from demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java rename to demo/src/main/java/com/google/android/exoplayer/demo/simple/DashRendererBuilder.java index 547ca0fefc..46923c6b74 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/DashRendererBuilder.java @@ -40,22 +40,26 @@ import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.Util; import android.media.MediaCodec; import android.os.Handler; import java.io.IOException; import java.util.ArrayList; +import java.util.List; /** - * A {@link RendererBuilder} for DASH VOD. + * A {@link RendererBuilder} for DASH. */ -/* package */ class DashVodRendererBuilder implements RendererBuilder, +/* package */ class DashRendererBuilder implements RendererBuilder, ManifestCallback { 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; @@ -63,8 +67,9 @@ import java.util.ArrayList; private final String contentId; private RendererBuilderCallback callback; + private ManifestFetcher manifestFetcher; - public DashVodRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url, + public DashRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url, String contentId) { this.playerActivity = playerActivity; this.userAgent = userAgent; @@ -76,8 +81,8 @@ import java.util.ArrayList; public void buildRenderers(RendererBuilderCallback callback) { this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, contentId, url, userAgent); + manifestFetcher = new ManifestFetcher(parser, contentId, url, + userAgent); manifestFetcher.singleLoad(playerActivity.getMainLooper(), this); } @@ -88,48 +93,50 @@ import java.util.ArrayList; @Override public void onManifest(String contentId, MediaPresentationDescription manifest) { + Period period = manifest.periods.get(0); Handler mainHandler = playerActivity.getMainHandler(); LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - // Obtain Representations for playback. + // Determine which video representations we should use for playback. int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); - Representation audioRepresentation = null; - ArrayList videoRepresentationsList = new ArrayList(); - Period period = manifest.periods.get(0); - for (int i = 0; i < period.adaptationSets.size(); i++) { - AdaptationSet adaptationSet = period.adaptationSets.get(i); - int adaptationSetType = adaptationSet.type; - for (int j = 0; j < adaptationSet.representations.size(); j++) { - Representation representation = adaptationSet.representations.get(j); - if (audioRepresentation == null && adaptationSetType == AdaptationSet.TYPE_AUDIO) { - audioRepresentation = representation; - } else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { - Format format = representation.format; - if (format.width * format.height <= maxDecodableFrameSize) { - videoRepresentationsList.add(representation); - } else { - // The device isn't capable of playing this stream. - } - } + int videoAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_VIDEO); + List videoRepresentations = + period.adaptationSets.get(videoAdaptationSetIndex).representations; + ArrayList videoRepresentationIndexList = new ArrayList(); + for (int i = 0; i < videoRepresentations.size(); i++) { + Format format = videoRepresentations.get(i).format; + if (format.width * format.height > maxDecodableFrameSize) { + // Filtering stream that device cannot play + } else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4) + && !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) { + // Filtering unsupported mime type + } else { + videoRepresentationIndexList.add(i); } } - Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()]; - videoRepresentationsList.toArray(videoRepresentations); // Build the video renderer. - DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); - ChunkSource videoChunkSource = new DashChunkSource(videoDataSource, - new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); - ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, - VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); - MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, - MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); + final MediaCodecVideoTrackRenderer videoRenderer; + if (videoRepresentationIndexList.isEmpty()) { + videoRenderer = null; + } else { + int[] videoRepresentationIndices = Util.toArray(videoRepresentationIndexList); + DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); + ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex, + videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter), + LIVE_EDGE_LATENCY_MS); + ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, + VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); + videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); + } // Build the audio renderer. + int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO); DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); - ChunkSource audioChunkSource = new DashChunkSource(audioDataSource, - new FormatEvaluator.FixedEvaluator(), audioRepresentation); + ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher, audioAdaptationSetIndex, + 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/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java index 1622998ae4..8c47dea3c1 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java @@ -61,10 +61,6 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call private static final String TAG = "PlayerActivity"; - public static final int TYPE_DASH_VOD = 0; - public static final int TYPE_SS_VOD = 1; - public static final int TYPE_OTHER = 2; - private MediaController mediaController; private Handler mainHandler; private View shutterView; @@ -90,7 +86,7 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call Intent intent = getIntent(); contentUri = intent.getData(); - contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, TYPE_OTHER); + contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, DemoUtil.TYPE_OTHER); contentId = intent.getStringExtra(DemoUtil.CONTENT_ID_EXTRA); mainHandler = new Handler(getMainLooper()); @@ -165,11 +161,11 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call private RendererBuilder getRendererBuilder() { String userAgent = DemoUtil.getUserAgent(this); switch (contentType) { - case TYPE_SS_VOD: + case DemoUtil.TYPE_SS: return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(), contentId); - case TYPE_DASH_VOD: - return new DashVodRendererBuilder(this, userAgent, contentUri.toString(), contentId); + case DemoUtil.TYPE_DASH: + return new DashRendererBuilder(this, userAgent, contentUri.toString(), contentId); default: return new DefaultRendererBuilder(this, contentUri); } 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 8686fa3b5a..90a06a6216 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 @@ -38,6 +38,7 @@ import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; +import com.google.android.exoplayer.util.Util; import android.media.MediaCodec; import android.os.Handler; @@ -115,10 +116,7 @@ import java.util.ArrayList; } } } - int[] videoTrackIndices = new int[videoTrackIndexList.size()]; - for (int i = 0; i < videoTrackIndexList.size(); i++) { - videoTrackIndices[i] = videoTrackIndexList.get(i); - } + int[] videoTrackIndices = Util.toArray(videoTrackIndexList); // Build the video renderer. DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java index 2c7cf33649..ce9965f313 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java @@ -46,6 +46,10 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent { this.selectedSource = sources[0]; } + public MultiTrackChunkSource(List sources) { + this(toChunkSourceArray(sources)); + } + /** * Gets the number of tracks that this source can switch between. May be called safely from any * thread. @@ -107,4 +111,10 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent { selectedSource.onChunkLoadError(chunk, e); } + private static ChunkSource[] toChunkSourceArray(List sources) { + ChunkSource[] chunkSourceArray = new ChunkSource[sources.size()]; + sources.toArray(chunkSourceArray); + return chunkSourceArray; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index f663457ce7..9bcb1aa3b8 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer.dash; +import com.google.android.exoplayer.BehindLiveWindowException; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.TrackInfo; +import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.ChunkSource; @@ -27,76 +29,175 @@ 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.chunk.SingleSampleMediaChunk; +import com.google.android.exoplayer.dash.mpd.AdaptationSet; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; +import com.google.android.exoplayer.dash.mpd.Period; 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.text.webvtt.WebvttParser; 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.ManifestFetcher; import com.google.android.exoplayer.util.MimeTypes; import android.net.Uri; +import android.os.SystemClock; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; /** * An {@link ChunkSource} for DASH streams. *

- * This implementation currently supports fMP4 and webm. + * This implementation currently supports fMP4, webm, and webvtt. */ public class DashChunkSource implements ChunkSource { + /** + * Thrown when an AdaptationSet is missing from the MPD. + */ + public static class NoAdaptationSetException extends IOException { + + public NoAdaptationSetException(String message) { + super(message); + } + + } + + /** + * Specifies that we should process all tracks. + */ + public static final int USE_ALL_TRACKS = -1; + private final TrackInfo trackInfo; private final DataSource dataSource; private final FormatEvaluator evaluator; private final Evaluation evaluation; + private final StringBuilder headerBuilder; + private final long liveEdgeLatencyUs; 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 final HashMap representationHolders; + + private final ManifestFetcher manifestFetcher; + private final int adaptationSetIndex; + private final int[] representationIndices; + + private MediaPresentationDescription currentManifest; + private boolean finishedCurrentManifest; private boolean lastChunkWasInitialization; + private IOException fatalError; /** + * Lightweight constructor to use for fixed duration content. + * * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param evaluator Selects from the available formats. + * @param formatEvaluator Selects from the available formats. * @param representations The representations to be considered by the source. */ - public DashChunkSource(DataSource dataSource, FormatEvaluator evaluator, + public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator, Representation... representations) { - long periodDurationUs = (representations[0].periodDurationMs == -1) - ? -1 : representations[0].periodDurationMs * 1000; + this(buildManifest(Arrays.asList(representations)), 0, null, dataSource, formatEvaluator); + } + /** + * Lightweight constructor to use for fixed duration content. + * + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param formatEvaluator Selects from the available formats. + * @param representations The representations to be considered by the source. + */ + public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator, + List representations) { + this(buildManifest(representations), 0, null, dataSource, formatEvaluator); + } + + /** + * Constructor to use for fixed duration content. + * + * @param manifest The manifest. + * @param adaptationSetIndex The index of the adaptation set that should be used. + * @param representationIndices The indices of the representations within the adaptations set + * that should be used. May be null if all representations within the adaptation set should + * be considered. + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param formatEvaluator Selects from the available formats. + */ + public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex, + int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator) { + this(null, manifest, adaptationSetIndex, representationIndices, dataSource, formatEvaluator, 0); + } + + /** + * 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 adaptationSetIndex The index of the adaptation set that should be used. + * @param representationIndices The indices of the representations within the adaptations set + * that should be used. May be null if all representations within the adaptation set 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 DashChunkSource(ManifestFetcher manifestFetcher, + int adaptationSetIndex, int[] representationIndices, DataSource dataSource, + FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) { + this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices, + dataSource, formatEvaluator, liveEdgeLatencyMs * 1000); + } + + private DashChunkSource(ManifestFetcher manifestFetcher, + MediaPresentationDescription initialManifest, int adaptationSetIndex, + int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator, + long liveEdgeLatencyUs) { + this.manifestFetcher = manifestFetcher; + this.currentManifest = initialManifest; + this.adaptationSetIndex = adaptationSetIndex; + this.representationIndices = representationIndices; 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, periodDurationUs); + this.evaluator = formatEvaluator; + this.liveEdgeLatencyUs = liveEdgeLatencyUs; this.evaluation = new Evaluation(); + this.headerBuilder = new StringBuilder(); + + Representation[] representations = getFilteredRepresentations(currentManifest, + adaptationSetIndex, representationIndices); + long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US) + ? TrackRenderer.UNKNOWN_TIME_US : representations[0].periodDurationMs * 1000; + this.trackInfo = new TrackInfo(representations[0].format.mimeType, periodDurationUs); + + this.formats = new Format[representations.length]; + this.representationHolders = new HashMap(); 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); - Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) - ? 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) { - segmentIndexes.put(formats[i].id, segmentIndex); - } + Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor() + : new FragmentedMp4Extractor(); + representationHolders.put(formats[i].id, + new RepresentationHolder(representations[i], extractor)); } this.maxWidth = maxWidth; this.maxHeight = maxHeight; @@ -118,21 +219,67 @@ public class DashChunkSource implements ChunkSource { @Override public void enable() { evaluator.enable(); + if (manifestFetcher != null) { + manifestFetcher.enable(); + } } @Override public void disable(List queue) { evaluator.disable(); + if (manifestFetcher != null) { + manifestFetcher.disable(); + } } @Override public void continueBuffering(long playbackPositionUs) { - // Do nothing + if (manifestFetcher == null || !currentManifest.dynamic || fatalError != null) { + return; + } + + MediaPresentationDescription newManifest = manifestFetcher.getManifest(); + if (currentManifest != newManifest && newManifest != null) { + Representation[] newRepresentations = DashChunkSource.getFilteredRepresentations(newManifest, + adaptationSetIndex, representationIndices); + for (Representation representation : newRepresentations) { + RepresentationHolder representationHolder = + representationHolders.get(representation.format.id); + DashSegmentIndex oldIndex = representationHolder.segmentIndex; + DashSegmentIndex newIndex = representation.getIndex(); + int newFirstSegmentNum = newIndex.getFirstSegmentNum(); + int segmentNumShift = oldIndex.getSegmentNum(newIndex.getTimeUs(newFirstSegmentNum)) + - newFirstSegmentNum; + representationHolder.segmentNumShift += segmentNumShift; + representationHolder.segmentIndex = newIndex; + } + currentManifest = newManifest; + finishedCurrentManifest = false; + } + + // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where + // minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit + // signaling in the stream, according to: + // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/ + long minUpdatePeriod = currentManifest.minUpdatePeriod; + if (minUpdatePeriod == 0) { + minUpdatePeriod = 5000; + } + + if (finishedCurrentManifest && (SystemClock.elapsedRealtime() + > manifestFetcher.getManifestLoadTimestamp() + minUpdatePeriod)) { + 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(); if (evaluation.format == null || !lastChunkWasInitialization) { evaluator.evaluate(queue, playbackPositionUs, formats, evaluation); @@ -150,17 +297,21 @@ public class DashChunkSource implements ChunkSource { return; } - Representation selectedRepresentation = representations.get(selectedFormat.id); - Extractor extractor = extractors.get(selectedRepresentation.format.id); + RepresentationHolder representationHolder = representationHolders.get(selectedFormat.id); + Representation selectedRepresentation = representationHolder.representation; + DashSegmentIndex segmentIndex = representationHolder.segmentIndex; + Extractor extractor = representationHolder.extractor; RangedUri pendingInitializationUri = null; RangedUri pendingIndexUri = null; + if (extractor.getFormat() == null) { pendingInitializationUri = selectedRepresentation.getInitializationUri(); } - if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) { + if (segmentIndex == null) { pendingIndexUri = selectedRepresentation.getIndexUri(); } + if (pendingInitializationUri != null || pendingIndexUri != null) { // We have initialization and/or index requests to make. Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri, @@ -170,28 +321,48 @@ public class DashChunkSource implements ChunkSource { return; } - int nextSegmentNum; - DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id); + int segmentNum; if (queue.isEmpty()) { - nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs); + if (currentManifest.dynamic) { + seekPositionUs = getLiveSeekPosition(); + } + segmentNum = segmentIndex.getSegmentNum(seekPositionUs); } else { - nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex; + segmentNum = queue.get(out.queueSize - 1).nextChunkIndex + - representationHolder.segmentNumShift; } - if (nextSegmentNum == -1) { + if (currentManifest.dynamic) { + if (segmentNum < segmentIndex.getFirstSegmentNum()) { + // This is before the first chunk in the current manifest. + fatalError = new BehindLiveWindowException(); + return; + } else if (segmentNum > segmentIndex.getLastSegmentNum()) { + // This is beyond the last chunk in the current manifest. + finishedCurrentManifest = true; + return; + } else if (segmentNum == segmentIndex.getLastSegmentNum()) { + // This is the last chunk in the current manifest. Mark the manifest as being finished, + // but continue to return the final chunk. + finishedCurrentManifest = true; + } + } + + if (segmentNum == -1) { out.chunk = null; return; } - Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor, - dataSource, nextSegmentNum, evaluation.trigger); + Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, segmentNum, + evaluation.trigger); lastChunkWasInitialization = false; out.chunk = nextMediaChunk; } @Override public IOException getError() { - return null; + return fatalError != null ? fatalError + : (manifestFetcher != null ? manifestFetcher.getError() : null); } @Override @@ -231,22 +402,90 @@ public class DashChunkSource implements ChunkSource { } DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, representation.getCacheKey()); + return new InitializationLoadable(dataSource, dataSpec, trigger, representation.format, extractor, expectedExtractorResult, indexAnchor); } - private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex, - Extractor extractor, DataSource dataSource, int segmentNum, int trigger) { - int lastSegmentNum = segmentIndex.getLastSegmentNum(); - int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1; + private Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, + int segmentNum, int trigger) { + Representation representation = representationHolder.representation; + DashSegmentIndex segmentIndex = representationHolder.segmentIndex; + long startTimeUs = segmentIndex.getTimeUs(segmentNum); - long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1) - : startTimeUs + segmentIndex.getDurationUs(segmentNum); + long endTimeUs = startTimeUs + segmentIndex.getDurationUs(segmentNum); + + boolean isLastSegment = !currentManifest.dynamic + && segmentNum == segmentIndex.getLastSegmentNum(); + int nextAbsoluteSegmentNum = isLastSegment ? -1 + : (representationHolder.segmentNumShift + segmentNum + 1); + RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum); DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, representation.getCacheKey()); - return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs, - endTimeUs, nextSegmentNum, extractor, false, 0); + + long presentationTimeOffsetUs = representation.presentationTimeOffsetMs * 1000; + if (representation.format.mimeType.equals(MimeTypes.TEXT_VTT)) { + if (representationHolder.vttHeaderOffsetUs != presentationTimeOffsetUs) { + // Update the VTT header. + headerBuilder.setLength(0); + headerBuilder.append(WebvttParser.EXO_HEADER).append("=") + .append(WebvttParser.OFFSET).append(presentationTimeOffsetUs).append("\n"); + representationHolder.vttHeader = headerBuilder.toString().getBytes(); + representationHolder.vttHeaderOffsetUs = presentationTimeOffsetUs; + } + return new SingleSampleMediaChunk(dataSource, dataSpec, representation.format, 0, + startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader); + } else { + return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs, + endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, false, + presentationTimeOffsetUs); + } + } + + /** + * 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 (RepresentationHolder representationHolder : representationHolders.values()) { + DashSegmentIndex segmentIndex = representationHolder.segmentIndex; + int lastSegmentNum = segmentIndex.getLastSegmentNum(); + long indexLiveEdgeTimestampUs = segmentIndex.getTimeUs(lastSegmentNum) + + segmentIndex.getDurationUs(lastSegmentNum); + liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, indexLiveEdgeTimestampUs); + } + return liveEdgeTimestampUs - liveEdgeLatencyUs; + } + + private static Representation[] getFilteredRepresentations(MediaPresentationDescription manifest, + int adaptationSetIndex, int[] representationIndices) { + List representations = + manifest.periods.get(0).adaptationSets.get(adaptationSetIndex).representations; + if (representationIndices == null) { + Representation[] filteredRepresentations = new Representation[representations.size()]; + representations.toArray(filteredRepresentations); + return filteredRepresentations; + } else { + Representation[] filteredRepresentations = new Representation[representationIndices.length]; + for (int i = 0; i < representationIndices.length; i++) { + filteredRepresentations[i] = representations.get(representationIndices[i]); + } + return filteredRepresentations; + } + } + + private static MediaPresentationDescription buildManifest(List representations) { + Representation firstRepresentation = representations.get(0); + AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); + Period period = new Period(null, firstRepresentation.periodStartMs, + firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet)); + long duration = firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs; + return new MediaPresentationDescription(-1, duration, -1, false, -1, -1, null, + Collections.singletonList(period)); } private class InitializationLoadable extends Chunk { @@ -274,11 +513,30 @@ public class DashChunkSource implements ChunkSource { + expectedExtractorResult + ", got " + result); } if ((result & Extractor.RESULT_READ_INDEX) != 0) { - segmentIndexes.put(format.id, - new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor)); + representationHolders.get(format.id).segmentIndex = + new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor); } } } + private static class RepresentationHolder { + + public final Representation representation; + public final Extractor extractor; + + public DashSegmentIndex segmentIndex; + public int segmentNumShift; + + public long vttHeaderOffsetUs; + public byte[] vttHeader; + + public RepresentationHolder(Representation representation, Extractor extractor) { + this.representation = representation; + this.extractor = extractor; + this.segmentIndex = representation.getIndex(); + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java index 3e64cb9853..c1cc738661 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java @@ -56,4 +56,21 @@ public class Period { this.adaptationSets = Collections.unmodifiableList(adaptationSets); } + /** + * Returns the index of the first adaptation set of a given type, or -1 if no adaptation set of + * the specified type exists. + * + * @param type An adaptation set type. + * @return The index of the first adaptation set of the specified type, or -1. + */ + public int getAdaptationSetIndex(int type) { + int adaptationCount = adaptationSets.size(); + for (int i = 0; i < adaptationCount; i++) { + if (adaptationSets.get(i).type == type) { + return i; + } + } + return -1; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java index 9cf8274207..9aed794b22 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer.util; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; +import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.util.Pair; @@ -29,12 +30,25 @@ import java.net.URLConnection; import java.util.concurrent.CancellationException; /** - * Performs both single and repeated loads of media manfifests. + * Performs both single and repeated loads of media manifests. * * @param The type of manifest. */ public class ManifestFetcher implements Loader.Callback { + /** + * Interface definition for a callback to be notified of {@link ManifestFetcher} events. + */ + public interface EventListener { + + public void onManifestRefreshStarted(); + + public void onManifestRefreshed(); + + public void onManifestError(IOException e); + + } + /** * Callback for the result of a single load. * @@ -61,9 +75,12 @@ public class ManifestFetcher implements Loader.Callback { } /* package */ final ManifestParser parser; - /* package */ final String manifestUrl; /* package */ final String contentId; /* package */ final String userAgent; + private final Handler eventHandler; + private final EventListener eventListener; + + /* package */ volatile String manifestUrl; private int enabledCount; private Loader loader; @@ -76,6 +93,11 @@ public class ManifestFetcher implements Loader.Callback { private volatile T manifest; private volatile long manifestLoadTimestamp; + public ManifestFetcher(ManifestParser parser, String contentId, String manifestUrl, + String userAgent) { + this(parser, contentId, manifestUrl, userAgent, null, null); + } + /** * @param parser A parser to parse the loaded manifest data. * @param contentId The content id of the content being loaded. May be null. @@ -83,11 +105,22 @@ public class ManifestFetcher implements Loader.Callback { * @param userAgent The User-Agent string that should be used. */ public ManifestFetcher(ManifestParser parser, String contentId, String manifestUrl, - String userAgent) { + String userAgent, Handler eventHandler, EventListener eventListener) { this.parser = parser; this.contentId = contentId; this.manifestUrl = manifestUrl; this.userAgent = userAgent; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + } + + /** + * Updates the manifest location. + * + * @param manifestUrl The manifest location. + */ + public void updateManifestUrl(String manifestUrl) { + this.manifestUrl = manifestUrl; } /** @@ -173,6 +206,7 @@ public class ManifestFetcher implements Loader.Callback { if (!loader.isLoading()) { currentLoadable = new ManifestLoadable(); loader.startLoading(currentLoadable, this); + notifyManifestRefreshStarted(); } } @@ -187,6 +221,8 @@ public class ManifestFetcher implements Loader.Callback { manifestLoadTimestamp = SystemClock.elapsedRealtime(); loadExceptionCount = 0; loadException = null; + + notifyManifestRefreshed(); } @Override @@ -204,12 +240,47 @@ public class ManifestFetcher implements Loader.Callback { loadExceptionCount++; loadExceptionTimestamp = SystemClock.elapsedRealtime(); loadException = new IOException(exception); + + notifyManifestError(loadException); } private long getRetryDelayMillis(long errorCount) { return Math.min((errorCount - 1) * 1000, 5000); } + private void notifyManifestRefreshStarted() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onManifestRefreshStarted(); + } + }); + } + } + + private void notifyManifestRefreshed() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onManifestRefreshed(); + } + }); + } + } + + private void notifyManifestError(final IOException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onManifestError(e); + } + }); + } + } + private class SingleFetchHelper implements Loader.Callback { private final Looper callbackLooper; diff --git a/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java b/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java index 17027a8d91..b9c4899de2 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java +++ b/library/src/main/java/com/google/android/exoplayer/util/PlayerControl.java @@ -70,12 +70,14 @@ public class PlayerControl implements MediaPlayerControl { @Override public int getCurrentPosition() { - return (int) exoPlayer.getCurrentPosition(); + return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 + : (int) exoPlayer.getCurrentPosition(); } @Override public int getDuration() { - return (int) exoPlayer.getDuration(); + return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 + : (int) exoPlayer.getDuration(); } @Override @@ -95,8 +97,9 @@ public class PlayerControl implements MediaPlayerControl { @Override public void seekTo(int timeMillis) { - // MediaController arrow keys generate unbounded values. - exoPlayer.seekTo(Math.min(Math.max(0, timeMillis), getDuration())); + long seekPosition = exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 + : Math.min(Math.max(0, timeMillis), getDuration()); + exoPlayer.seekTo(seekPosition); } } diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index b8cd40215d..95f9576d41 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -399,4 +399,22 @@ public final class Util { return scaledTimestamps; } + /** + * Converts a list of integers to a primitive array. + * + * @param list A list of integers. + * @return The list in array form, or null if the input list was null. + */ + public static int[] toArray(List list) { + if (list == null) { + return null; + } + int length = list.size(); + int[] intArray = new int[length]; + for (int i = 0; i < length; i++) { + intArray[i] = list.get(i); + } + return intArray; + } + }