Commit 511dd943 by Oliver Woodman

Merge branch 'dev' into dev-hls

parents c2d55aca 4efc0abd
Showing with 731 additions and 213 deletions
...@@ -47,7 +47,7 @@ public class DemoUtil { ...@@ -47,7 +47,7 @@ public class DemoUtil {
public static final String CONTENT_TYPE_EXTRA = "content_type"; public static final String CONTENT_TYPE_EXTRA = "content_type";
public static final String CONTENT_ID_EXTRA = "content_id"; 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_SS = 1;
public static final int TYPE_OTHER = 2; public static final int TYPE_OTHER = 2;
public static final int TYPE_DASH_LIVE = 3; public static final int TYPE_DASH_LIVE = 3;
......
...@@ -46,13 +46,13 @@ package com.google.android.exoplayer.demo; ...@@ -46,13 +46,13 @@ package com.google.android.exoplayer.demo;
"http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" "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&" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&"
+ "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D." + "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D."
+ "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH_VOD, false, + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false,
false), false),
new Sample("Google Play (DASH)", "3aa39fa2cc27967f", new Sample("Google Play (DASH)", "3aa39fa2cc27967f",
"http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" "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&" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&"
+ "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A." + "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A."
+ "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH_VOD, false, + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false,
false), false),
new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed", new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed",
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism", "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism",
...@@ -69,13 +69,13 @@ package com.google.android.exoplayer.demo; ...@@ -69,13 +69,13 @@ package com.google.android.exoplayer.demo;
"http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" "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&" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&"
+ "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D." + "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D."
+ "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH_VOD, false, + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false,
true), true),
new Sample("Google Play", "3aa39fa2cc27967f", new Sample("Google Play", "3aa39fa2cc27967f",
"http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" "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&" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&"
+ "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A." + "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A."
+ "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH_VOD, false, + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false,
true), true),
}; };
...@@ -84,12 +84,12 @@ package com.google.android.exoplayer.demo; ...@@ -84,12 +84,12 @@ package com.google.android.exoplayer.demo;
"http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" "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&" + "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&"
+ "expire=19000000000&signature=A3EC7EE53ABE601B357F7CAB8B54AD0702CA85A7." + "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", new Sample("Google Play", "3aa39fa2cc27967f",
"http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" "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&" + "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&"
+ "expire=19000000000&signature=B752B262C6D7262EC4E4EB67901E5D8F7058A81D." + "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[] { public static final Sample[] SMOOTHSTREAMING = new Sample[] {
...@@ -106,32 +106,32 @@ package com.google.android.exoplayer.demo; ...@@ -106,32 +106,32 @@ package com.google.android.exoplayer.demo;
"http://www.youtube.com/api/manifest/dash/id/d286538032258a1c/source/youtube?" "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" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=41EA40A027A125A16292E0A5E3277A3B5FA9B938." + "&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", new Sample("WV: HDCP not required", "48fcc369939ac96c",
"http://www.youtube.com/api/manifest/dash/id/48fcc369939ac96c/source/youtube?" "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" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=315911BDCEED0FB0C763455BDCC97449DAAFA9E8." + "&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", new Sample("WV: HDCP required", "e06c39f1151da3df",
"http://www.youtube.com/api/manifest/dash/id/e06c39f1151da3df/source/youtube?" "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" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=A47A1E13E7243BD567601A75F79B34644D0DC592." + "&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", new Sample("WV: Secure video path required", "0894c7c8719b28a0",
"http://www.youtube.com/api/manifest/dash/id/0894c7c8719b28a0/source/youtube?" "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" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=2847EE498970F6B45176766CD2802FEB4D4CB7B2." + "&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", new Sample("WV: HDCP + secure video path required", "efd045b1eb61888a",
"http://www.youtube.com/api/manifest/dash/id/efd045b1eb61888a/source/youtube?" "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" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=61611F115EEEC7BADE5536827343FFFE2D83D14F." + "&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", new Sample("WV: 30s license duration", "f9a34cab7b05881a",
"http://www.youtube.com/api/manifest/dash/id/f9a34cab7b05881a/source/youtube?" "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" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6." + "&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[] HLS = new Sample[] { public static final Sample[] HLS = new Sample[] {
......
...@@ -19,7 +19,7 @@ import com.google.android.exoplayer.ExoPlayer; ...@@ -19,7 +19,7 @@ import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.VideoSurfaceView; import com.google.android.exoplayer.VideoSurfaceView;
import com.google.android.exoplayer.demo.DemoUtil; import com.google.android.exoplayer.demo.DemoUtil;
import com.google.android.exoplayer.demo.R; 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.DefaultRendererBuilder;
import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.demo.full.player.DemoPlayer;
import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder;
...@@ -179,8 +179,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba ...@@ -179,8 +179,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
case DemoUtil.TYPE_SS: case DemoUtil.TYPE_SS:
return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), contentId, return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), contentId,
new SmoothStreamingTestMediaDrmCallback(), debugTextView); new SmoothStreamingTestMediaDrmCallback(), debugTextView);
case DemoUtil.TYPE_DASH_VOD: case DemoUtil.TYPE_DASH:
return new DashVodRendererBuilder(userAgent, contentUri.toString(), contentId, return new DashRendererBuilder(userAgent, contentUri.toString(), contentId,
new WidevineTestMediaDrmCallback(contentId), debugTextView); new WidevineTestMediaDrmCallback(contentId), debugTextView);
case DemoUtil.TYPE_HLS: case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId); return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId);
......
...@@ -40,6 +40,8 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderC ...@@ -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.DrmSessionManager;
import com.google.android.exoplayer.drm.MediaDrmCallback; import com.google.android.exoplayer.drm.MediaDrmCallback;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager; 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.BufferPool;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
...@@ -58,16 +60,19 @@ import android.widget.TextView; ...@@ -58,16 +60,19 @@ import android.widget.TextView;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; 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<MediaPresentationDescription> { ManifestCallback<MediaPresentationDescription> {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int VIDEO_BUFFER_SEGMENTS = 200;
private static final int AUDIO_BUFFER_SEGMENTS = 60; 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_UNKNOWN = -1;
private static final int SECURITY_LEVEL_1 = 1; private static final int SECURITY_LEVEL_1 = 1;
...@@ -81,8 +86,9 @@ public class DashVodRendererBuilder implements RendererBuilder, ...@@ -81,8 +86,9 @@ public class DashVodRendererBuilder implements RendererBuilder,
private DemoPlayer player; private DemoPlayer player;
private RendererBuilderCallback callback; private RendererBuilderCallback callback;
private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
public DashVodRendererBuilder(String userAgent, String url, String contentId, public DashRendererBuilder(String userAgent, String url, String contentId,
MediaDrmCallback drmCallback, TextView debugTextView) { MediaDrmCallback drmCallback, TextView debugTextView) {
this.userAgent = userAgent; this.userAgent = userAgent;
this.url = url; this.url = url;
...@@ -96,8 +102,8 @@ public class DashVodRendererBuilder implements RendererBuilder, ...@@ -96,8 +102,8 @@ public class DashVodRendererBuilder implements RendererBuilder,
this.player = player; this.player = player;
this.callback = callback; this.callback = callback;
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
ManifestFetcher<MediaPresentationDescription> manifestFetcher = manifestFetcher = new ManifestFetcher<MediaPresentationDescription>(parser, contentId, url,
new ManifestFetcher<MediaPresentationDescription>(parser, contentId, url, userAgent); userAgent);
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
} }
...@@ -108,38 +114,17 @@ public class DashVodRendererBuilder implements RendererBuilder, ...@@ -108,38 +114,17 @@ public class DashVodRendererBuilder implements RendererBuilder,
@Override @Override
public void onManifest(String contentId, MediaPresentationDescription manifest) { public void onManifest(String contentId, MediaPresentationDescription manifest) {
Period period = manifest.periods.get(0);
Handler mainHandler = player.getMainHandler(); Handler mainHandler = player.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
// Obtain Representations for playback. int videoAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_VIDEO);
int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); AdaptationSet videoAdaptationSet = period.adaptationSets.get(videoAdaptationSetIndex);
ArrayList<Representation> audioRepresentationsList = new ArrayList<Representation>();
ArrayList<Representation> videoRepresentationsList = new ArrayList<Representation>();
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);
// Check drm support if necessary. // Check drm support if necessary.
boolean hasContentProtection = videoAdaptationSet.hasContentProtection();
boolean filterHdContent = false;
DrmSessionManager drmSessionManager = null; DrmSessionManager drmSessionManager = null;
if (hasContentProtection) { if (hasContentProtection) {
if (Util.SDK_INT < 18) { if (Util.SDK_INT < 18) {
...@@ -151,55 +136,81 @@ public class DashVodRendererBuilder implements RendererBuilder, ...@@ -151,55 +136,81 @@ public class DashVodRendererBuilder implements RendererBuilder,
Pair<DrmSessionManager, Boolean> drmSessionManagerData = Pair<DrmSessionManager, Boolean> drmSessionManagerData =
V18Compat.getDrmSessionManagerData(player, drmCallback); V18Compat.getDrmSessionManagerData(player, drmCallback);
drmSessionManager = drmSessionManagerData.first; drmSessionManager = drmSessionManagerData.first;
if (!drmSessionManagerData.second) {
// HD streams require L1 security. // HD streams require L1 security.
videoRepresentations = getSdRepresentations(videoRepresentations); filterHdContent = !drmSessionManagerData.second;
}
} catch (Exception e) { } catch (Exception e) {
callback.onRenderersError(e); callback.onRenderersError(e);
return; return;
} }
} }
// Build the video renderer. // Determine which video representations we should use for playback.
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
ChunkSource videoChunkSource; List<Representation> videoRepresentations = videoAdaptationSet.representations;
String mimeType = videoRepresentations[0].format.mimeType; ArrayList<Integer> videoRepresentationIndexList = new ArrayList<Integer>();
if (mimeType.equals(MimeTypes.VIDEO_MP4) || mimeType.equals(MimeTypes.VIDEO_WEBM)) { for (int i = 0; i < videoRepresentations.size(); i++) {
videoChunkSource = new DashChunkSource(videoDataSource, Format format = videoRepresentations.get(i).format;
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); 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 { } else {
throw new IllegalStateException("Unexpected mime type: " + mimeType); 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, ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_VIDEO); DemoPlayer.TYPE_VIDEO);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true,
drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50);
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<ChunkSource> audioChunkSourceList = new ArrayList<ChunkSource>();
List<String> audioTrackNameList = new ArrayList<String>();
List<Representation> 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));
}
// Build the audio renderer. // Build the audio renderer.
final String[] audioTrackNames; final String[] audioTrackNames;
final MultiTrackChunkSource audioChunkSource; final MultiTrackChunkSource audioChunkSource;
final TrackRenderer audioRenderer; final TrackRenderer audioRenderer;
if (audioRepresentationsList.isEmpty()) { if (audioChunkSourceList.isEmpty()) {
audioTrackNames = null; audioTrackNames = null;
audioChunkSource = null; audioChunkSource = null;
audioRenderer = null; audioRenderer = null;
} else { } else {
DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); audioTrackNames = new String[audioTrackNameList.size()];
audioTrackNames = new String[audioRepresentationsList.size()]; audioTrackNameList.toArray(audioTrackNames);
ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()]; audioChunkSource = new MultiTrackChunkSource(audioChunkSourceList);
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);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_AUDIO); DemoPlayer.TYPE_AUDIO);
...@@ -207,37 +218,61 @@ public class DashVodRendererBuilder implements RendererBuilder, ...@@ -207,37 +218,61 @@ public class DashVodRendererBuilder implements RendererBuilder,
mainHandler, player); mainHandler, player);
} }
// Build the debug renderer. // Build the text chunk sources.
TrackRenderer debugRenderer = debugTextView != null DataSource textDataSource = new UriDataSource(userAgent, bandwidthMeter);
? new DebugTrackRenderer(debugTextView, videoRenderer, videoSampleSource) : null; FormatEvaluator textEvaluator = new FormatEvaluator.FixedEvaluator();
List<ChunkSource> textChunkSourceList = new ArrayList<ChunkSource>();
List<String> textTrackNameList = new ArrayList<String>();
for (int i = 0; i < period.adaptationSets.size(); i++) {
AdaptationSet adaptationSet = period.adaptationSets.get(i);
if (adaptationSet.type == AdaptationSet.TYPE_TEXT) {
List<Representation> 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. // Invoke the callback.
String[][] trackNames = new String[DemoPlayer.RENDERER_COUNT][]; String[][] trackNames = new String[DemoPlayer.RENDERER_COUNT][];
trackNames[DemoPlayer.TYPE_AUDIO] = audioTrackNames; trackNames[DemoPlayer.TYPE_AUDIO] = audioTrackNames;
trackNames[DemoPlayer.TYPE_TEXT] = textTrackNames;
MultiTrackChunkSource[] multiTrackChunkSources = MultiTrackChunkSource[] multiTrackChunkSources =
new MultiTrackChunkSource[DemoPlayer.RENDERER_COUNT]; new MultiTrackChunkSource[DemoPlayer.RENDERER_COUNT];
multiTrackChunkSources[DemoPlayer.TYPE_AUDIO] = audioChunkSource; multiTrackChunkSources[DemoPlayer.TYPE_AUDIO] = audioChunkSource;
multiTrackChunkSources[DemoPlayer.TYPE_TEXT] = textChunkSource;
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_TEXT] = textRenderer;
renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer;
callback.onRenderers(trackNames, multiTrackChunkSources, renderers); callback.onRenderers(trackNames, multiTrackChunkSources, renderers);
} }
private Representation[] getSdRepresentations(Representation[] representations) {
ArrayList<Representation> sdRepresentations = new ArrayList<Representation>();
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) @TargetApi(18)
private static class V18Compat { private static class V18Compat {
......
...@@ -64,7 +64,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, ...@@ -64,7 +64,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int VIDEO_BUFFER_SEGMENTS = 200;
private static final int AUDIO_BUFFER_SEGMENTS = 60; 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 static final int LIVE_EDGE_LATENCY_MS = 30000;
private final String userAgent; private final String userAgent;
...@@ -149,10 +149,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, ...@@ -149,10 +149,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
} }
} }
} }
int[] videoTrackIndices = new int[videoTrackIndexList.size()]; int[] videoTrackIndices = Util.toArray(videoTrackIndexList);
for (int i = 0; i < videoTrackIndexList.size(); i++) {
videoTrackIndices[i] = videoTrackIndexList.get(i);
}
// Build the video renderer. // Build the video renderer.
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
...@@ -221,7 +218,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, ...@@ -221,7 +218,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
} }
textChunkSource = new MultiTrackChunkSource(textChunkSources); textChunkSource = new MultiTrackChunkSource(textChunkSources);
ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl, 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); DemoPlayer.TYPE_TEXT);
textRenderer = new TextTrackRenderer(ttmlSampleSource, new TtmlParser(), player, textRenderer = new TextTrackRenderer(ttmlSampleSource, new TtmlParser(), player,
mainHandler.getLooper()); mainHandler.getLooper());
......
...@@ -40,22 +40,26 @@ import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; ...@@ -40,22 +40,26 @@ import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; 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.media.MediaCodec;
import android.os.Handler; import android.os.Handler;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; 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<MediaPresentationDescription> { ManifestCallback<MediaPresentationDescription> {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int VIDEO_BUFFER_SEGMENTS = 200;
private static final int AUDIO_BUFFER_SEGMENTS = 60; private static final int AUDIO_BUFFER_SEGMENTS = 60;
private static final int LIVE_EDGE_LATENCY_MS = 30000;
private final SimplePlayerActivity playerActivity; private final SimplePlayerActivity playerActivity;
private final String userAgent; private final String userAgent;
...@@ -63,8 +67,9 @@ import java.util.ArrayList; ...@@ -63,8 +67,9 @@ import java.util.ArrayList;
private final String contentId; private final String contentId;
private RendererBuilderCallback callback; private RendererBuilderCallback callback;
private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
public DashVodRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url, public DashRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url,
String contentId) { String contentId) {
this.playerActivity = playerActivity; this.playerActivity = playerActivity;
this.userAgent = userAgent; this.userAgent = userAgent;
...@@ -76,8 +81,8 @@ import java.util.ArrayList; ...@@ -76,8 +81,8 @@ import java.util.ArrayList;
public void buildRenderers(RendererBuilderCallback callback) { public void buildRenderers(RendererBuilderCallback callback) {
this.callback = callback; this.callback = callback;
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
ManifestFetcher<MediaPresentationDescription> manifestFetcher = manifestFetcher = new ManifestFetcher<MediaPresentationDescription>(parser, contentId, url,
new ManifestFetcher<MediaPresentationDescription>(parser, contentId, url, userAgent); userAgent);
manifestFetcher.singleLoad(playerActivity.getMainLooper(), this); manifestFetcher.singleLoad(playerActivity.getMainLooper(), this);
} }
...@@ -88,48 +93,50 @@ import java.util.ArrayList; ...@@ -88,48 +93,50 @@ import java.util.ArrayList;
@Override @Override
public void onManifest(String contentId, MediaPresentationDescription manifest) { public void onManifest(String contentId, MediaPresentationDescription manifest) {
Period period = manifest.periods.get(0);
Handler mainHandler = playerActivity.getMainHandler(); Handler mainHandler = playerActivity.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
// Obtain Representations for playback. // Determine which video representations we should use for playback.
int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
Representation audioRepresentation = null; int videoAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_VIDEO);
ArrayList<Representation> videoRepresentationsList = new ArrayList<Representation>(); List<Representation> videoRepresentations =
Period period = manifest.periods.get(0); period.adaptationSets.get(videoAdaptationSetIndex).representations;
for (int i = 0; i < period.adaptationSets.size(); i++) { ArrayList<Integer> videoRepresentationIndexList = new ArrayList<Integer>();
AdaptationSet adaptationSet = period.adaptationSets.get(i); for (int i = 0; i < videoRepresentations.size(); i++) {
int adaptationSetType = adaptationSet.type; Format format = videoRepresentations.get(i).format;
for (int j = 0; j < adaptationSet.representations.size(); j++) { if (format.width * format.height > maxDecodableFrameSize) {
Representation representation = adaptationSet.representations.get(j); // Filtering stream that device cannot play
if (audioRepresentation == null && adaptationSetType == AdaptationSet.TYPE_AUDIO) { } else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4)
audioRepresentation = representation; && !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) {
} else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { // Filtering unsupported mime type
Format format = representation.format;
if (format.width * format.height <= maxDecodableFrameSize) {
videoRepresentationsList.add(representation);
} else { } else {
// The device isn't capable of playing this stream. videoRepresentationIndexList.add(i);
}
} }
} }
}
Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()];
videoRepresentationsList.toArray(videoRepresentations);
// Build the video renderer. // Build the video renderer.
final MediaCodecVideoTrackRenderer videoRenderer;
if (videoRepresentationIndexList.isEmpty()) {
videoRenderer = null;
} else {
int[] videoRepresentationIndices = Util.toArray(videoRepresentationIndexList);
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource videoChunkSource = new DashChunkSource(videoDataSource, ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter),
LIVE_EDGE_LATENCY_MS);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
}
// Build the audio renderer. // Build the audio renderer.
int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO);
DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource audioChunkSource = new DashChunkSource(audioDataSource, ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher, audioAdaptationSetIndex,
new FormatEvaluator.FixedEvaluator(), audioRepresentation); new int[] {0}, audioDataSource, new FormatEvaluator.FixedEvaluator(), LIVE_EDGE_LATENCY_MS);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer( MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(
......
...@@ -164,8 +164,8 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call ...@@ -164,8 +164,8 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
case DemoUtil.TYPE_SS: case DemoUtil.TYPE_SS:
return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(), return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(),
contentId); contentId);
case DemoUtil.TYPE_DASH_VOD: case DemoUtil.TYPE_DASH:
return new DashVodRendererBuilder(this, userAgent, contentUri.toString(), contentId); return new DashRendererBuilder(this, userAgent, contentUri.toString(), contentId);
case DemoUtil.TYPE_HLS: case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(this, userAgent, contentUri.toString(), contentId); return new HlsRendererBuilder(this, userAgent, contentUri.toString(), contentId);
default: default:
......
...@@ -38,6 +38,7 @@ import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; ...@@ -38,6 +38,7 @@ import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import com.google.android.exoplayer.util.Util;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.os.Handler; import android.os.Handler;
...@@ -115,10 +116,7 @@ import java.util.ArrayList; ...@@ -115,10 +116,7 @@ import java.util.ArrayList;
} }
} }
} }
int[] videoTrackIndices = new int[videoTrackIndexList.size()]; int[] videoTrackIndices = Util.toArray(videoTrackIndexList);
for (int i = 0; i < videoTrackIndexList.size(); i++) {
videoTrackIndices[i] = videoTrackIndexList.get(i);
}
// Build the video renderer. // Build the video renderer.
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
......
...@@ -21,6 +21,16 @@ package com.google.android.exoplayer; ...@@ -21,6 +21,16 @@ package com.google.android.exoplayer;
public final class C { public final class C {
/** /**
* Represents an unknown microsecond time or duration.
*/
public static final long UNKNOWN_TIME_US = -1L;
/**
* The number of microseconds in one second.
*/
public static final long MICROS_PER_SECOND = 1000000L;
/**
* Represents an unbounded length of data. * Represents an unbounded length of data.
*/ */
public static final int LENGTH_UNBOUNDED = -1; public static final int LENGTH_UNBOUNDED = -1;
......
...@@ -71,10 +71,10 @@ public final class FrameworkSampleSource implements SampleSource { ...@@ -71,10 +71,10 @@ public final class FrameworkSampleSource implements SampleSource {
trackInfos = new TrackInfo[trackStates.length]; trackInfos = new TrackInfo[trackStates.length];
for (int i = 0; i < trackStates.length; i++) { for (int i = 0; i < trackStates.length; i++) {
android.media.MediaFormat format = extractor.getTrackFormat(i); android.media.MediaFormat format = extractor.getTrackFormat(i);
long duration = format.containsKey(android.media.MediaFormat.KEY_DURATION) ? long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION) ?
format.getLong(android.media.MediaFormat.KEY_DURATION) : TrackRenderer.UNKNOWN_TIME_US; format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
String mime = format.getString(android.media.MediaFormat.KEY_MIME); String mime = format.getString(android.media.MediaFormat.KEY_MIME);
trackInfos[i] = new TrackInfo(mime, duration); trackInfos[i] = new TrackInfo(mime, durationUs);
} }
prepared = true; prepared = true;
} }
......
...@@ -20,9 +20,21 @@ package com.google.android.exoplayer; ...@@ -20,9 +20,21 @@ package com.google.android.exoplayer;
*/ */
public final class TrackInfo { public final class TrackInfo {
/**
* The mime type.
*/
public final String mimeType; public final String mimeType;
/**
* The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration is unknown.
*/
public final long durationUs; public final long durationUs;
/**
* @param mimeType The mime type.
* @param durationUs The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration
* is unknown.
*/
public TrackInfo(String mimeType, long durationUs) { public TrackInfo(String mimeType, long durationUs) {
this.mimeType = mimeType; this.mimeType = mimeType;
this.durationUs = durationUs; this.durationUs = durationUs;
......
...@@ -67,9 +67,9 @@ public abstract class TrackRenderer implements ExoPlayerComponent { ...@@ -67,9 +67,9 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
protected static final int STATE_STARTED = 3; protected static final int STATE_STARTED = 3;
/** /**
* Represents an unknown time or duration. * Represents an unknown time or duration. Equal to {@link C#UNKNOWN_TIME_US}.
*/ */
public static final long UNKNOWN_TIME_US = -1; public static final long UNKNOWN_TIME_US = C.UNKNOWN_TIME_US; // -1
/** /**
* Represents a time or duration that should match the duration of the longest track whose * Represents a time or duration that should match the duration of the longest track whose
* duration is known. * duration is known.
......
...@@ -40,7 +40,7 @@ public final class AudioCapabilitiesReceiver { ...@@ -40,7 +40,7 @@ public final class AudioCapabilitiesReceiver {
} }
/** Default to stereo PCM on SDK <= 21 and when HDMI is unplugged. */ /** Default to stereo PCM on SDK < 21 and when HDMI is unplugged. */
private static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES = private static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES =
new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2); new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2);
......
...@@ -15,9 +15,11 @@ ...@@ -15,9 +15,11 @@
*/ */
package com.google.android.exoplayer.audio; package com.google.android.exoplayer.audio;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager; import android.media.AudioManager;
...@@ -80,22 +82,23 @@ public final class AudioTrack { ...@@ -80,22 +82,23 @@ public final class AudioTrack {
private static final String TAG = "AudioTrack"; private static final String TAG = "AudioTrack";
private static final long MICROS_PER_SECOND = 1000000L;
/** /**
* AudioTrack timestamps are deemed spurious if they are offset from the system clock by more * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
* than this amount. * than this amount.
* *
* <p>This is a fail safe that should not be required on correctly functioning devices. * <p>This is a fail safe that should not be required on correctly functioning devices.
*/ */
private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 10 * MICROS_PER_SECOND; private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 10 * C.MICROS_PER_SECOND;
/** /**
* AudioTrack latencies are deemed impossibly large if they are greater than this amount. * AudioTrack latencies are deemed impossibly large if they are greater than this amount.
* *
* <p>This is a fail safe that should not be required on correctly functioning devices. * <p>This is a fail safe that should not be required on correctly functioning devices.
*/ */
private static final long MAX_LATENCY_US = 10 * MICROS_PER_SECOND; private static final long MAX_LATENCY_US = 10 * C.MICROS_PER_SECOND;
/** Value for ac3Bitrate before the bitrate has been calculated. */
private static final int UNKNOWN_AC3_BITRATE = 0;
private static final int START_NOT_SET = 0; private static final int START_NOT_SET = 0;
private static final int START_IN_SYNC = 1; private static final int START_IN_SYNC = 1;
...@@ -139,6 +142,11 @@ public final class AudioTrack { ...@@ -139,6 +142,11 @@ public final class AudioTrack {
private int temporaryBufferOffset; private int temporaryBufferOffset;
private int temporaryBufferSize; private int temporaryBufferSize;
private boolean isAc3;
/** Bitrate measured in kilobits per second, if {@link #isAc3} is true. */
private int ac3Bitrate;
/** Constructs an audio track using the default minimum buffer size multiplier. */ /** Constructs an audio track using the default minimum buffer size multiplier. */
public AudioTrack() { public AudioTrack() {
this(DEFAULT_MIN_BUFFER_MULTIPLICATION_FACTOR); this(DEFAULT_MIN_BUFFER_MULTIPLICATION_FACTOR);
...@@ -277,6 +285,7 @@ public final class AudioTrack { ...@@ -277,6 +285,7 @@ public final class AudioTrack {
* @param bufferSize The total size of the playback buffer in bytes. Specify 0 to use a buffer * @param bufferSize The total size of the playback buffer in bytes. Specify 0 to use a buffer
* size based on the minimum for format. * size based on the minimum for format.
*/ */
@SuppressLint("InlinedApi")
public void reconfigure(MediaFormat format, int encoding, int bufferSize) { public void reconfigure(MediaFormat format, int encoding, int bufferSize) {
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int channelConfig; int channelConfig;
...@@ -300,8 +309,9 @@ public final class AudioTrack { ...@@ -300,8 +309,9 @@ public final class AudioTrack {
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
// TODO: Does channelConfig determine channelCount? // TODO: Does channelConfig determine channelCount?
boolean isAc3 = encoding == AudioFormat.ENCODING_AC3 || encoding == AudioFormat.ENCODING_E_AC3;
if (audioTrack != null && this.sampleRate == sampleRate if (audioTrack != null && this.sampleRate == sampleRate
&& this.channelConfig == channelConfig) { && this.channelConfig == channelConfig && !this.isAc3 && !isAc3) {
// We already have an existing audio track with the correct sample rate and channel config. // We already have an existing audio track with the correct sample rate and channel config.
return; return;
} }
...@@ -315,7 +325,8 @@ public final class AudioTrack { ...@@ -315,7 +325,8 @@ public final class AudioTrack {
bufferSize == 0 ? (int) (minBufferMultiplicationFactor * minBufferSize) : bufferSize; bufferSize == 0 ? (int) (minBufferMultiplicationFactor * minBufferSize) : bufferSize;
this.sampleRate = sampleRate; this.sampleRate = sampleRate;
this.channelConfig = channelConfig; this.channelConfig = channelConfig;
this.isAc3 = isAc3;
ac3Bitrate = UNKNOWN_AC3_BITRATE; // Calculated on receiving the first buffer if isAc3 is true.
frameSize = 2 * channelCount; // 2 bytes per 16 bit sample * number of channels. frameSize = 2 * channelCount; // 2 bytes per 16 bit sample * number of channels.
} }
...@@ -353,6 +364,14 @@ public final class AudioTrack { ...@@ -353,6 +364,14 @@ public final class AudioTrack {
int result = 0; int result = 0;
if (temporaryBufferSize == 0 && size != 0) { if (temporaryBufferSize == 0 && size != 0) {
if (isAc3 && ac3Bitrate == UNKNOWN_AC3_BITRATE) {
// Each AC-3 buffer contains 1536 frames of audio, so the AudioTrack playback position
// advances by 1536 per buffer (32 ms at 48 kHz). Calculate the bitrate in kbit/s.
int unscaledAc3Bitrate = size * 8 * sampleRate;
int divisor = 1000 * 1536;
ac3Bitrate = (unscaledAc3Bitrate + divisor / 2) / divisor;
}
// This is the first time we've seen this {@code buffer}. // This is the first time we've seen this {@code buffer}.
// Note: presentationTimeUs corresponds to the end of the sample, not the start. // Note: presentationTimeUs corresponds to the end of the sample, not the start.
long bufferStartTime = presentationTimeUs - framesToDurationUs(bytesToFrames(size)); long bufferStartTime = presentationTimeUs - framesToDurationUs(bytesToFrames(size));
...@@ -616,19 +635,24 @@ public final class AudioTrack { ...@@ -616,19 +635,24 @@ public final class AudioTrack {
} }
private long framesToBytes(long frameCount) { private long framesToBytes(long frameCount) {
// This method is unused on SDK >= 21.
return frameCount * frameSize; return frameCount * frameSize;
} }
private long bytesToFrames(long byteCount) { private long bytesToFrames(long byteCount) {
if (isAc3) {
return byteCount * 8 * sampleRate / (1000 * ac3Bitrate);
} else {
return byteCount / frameSize; return byteCount / frameSize;
} }
}
private long framesToDurationUs(long frameCount) { private long framesToDurationUs(long frameCount) {
return (frameCount * MICROS_PER_SECOND) / sampleRate; return (frameCount * C.MICROS_PER_SECOND) / sampleRate;
} }
private long durationUsToFrames(long durationUs) { private long durationUsToFrames(long durationUs) {
return (durationUs * sampleRate) / MICROS_PER_SECOND; return (durationUs * sampleRate) / C.MICROS_PER_SECOND;
} }
private void resetSyncParams() { private void resetSyncParams() {
......
...@@ -46,6 +46,10 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent { ...@@ -46,6 +46,10 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
this.selectedSource = sources[0]; this.selectedSource = sources[0];
} }
public MultiTrackChunkSource(List<ChunkSource> sources) {
this(toChunkSourceArray(sources));
}
/** /**
* Gets the number of tracks that this source can switch between. May be called safely from any * Gets the number of tracks that this source can switch between. May be called safely from any
* thread. * thread.
...@@ -107,4 +111,10 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent { ...@@ -107,4 +111,10 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
selectedSource.onChunkLoadError(chunk, e); selectedSource.onChunkLoadError(chunk, e);
} }
private static ChunkSource[] toChunkSourceArray(List<ChunkSource> sources) {
ChunkSource[] chunkSourceArray = new ChunkSource[sources.size()];
sources.toArray(chunkSourceArray);
return chunkSourceArray;
}
} }
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.chunk; package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
...@@ -42,7 +43,8 @@ public class SingleSampleChunkSource implements ChunkSource { ...@@ -42,7 +43,8 @@ public class SingleSampleChunkSource implements ChunkSource {
* @param dataSource A {@link DataSource} suitable for loading the sample data. * @param dataSource A {@link DataSource} suitable for loading the sample data.
* @param dataSpec Defines the location of the sample. * @param dataSpec Defines the location of the sample.
* @param format The format of the sample. * @param format The format of the sample.
* @param durationUs The duration of the sample in microseconds. * @param durationUs The duration of the sample in microseconds, or {@link C#UNKNOWN_TIME_US} if
* the duration is unknown.
* @param mediaFormat The sample media format. May be null. * @param mediaFormat The sample media format. May be null.
*/ */
public SingleSampleChunkSource(DataSource dataSource, DataSpec dataSpec, Format format, public SingleSampleChunkSource(DataSource dataSource, DataSpec dataSpec, Format format,
......
...@@ -15,9 +15,11 @@ ...@@ -15,9 +15,11 @@
*/ */
package com.google.android.exoplayer.dash; package com.google.android.exoplayer.dash;
import com.google.android.exoplayer.BehindLiveWindowException;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.TrackInfo; 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.Chunk;
import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.ChunkOperationHolder;
import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.ChunkSource;
...@@ -27,74 +29,175 @@ import com.google.android.exoplayer.chunk.FormatEvaluator; ...@@ -27,74 +29,175 @@ import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.Mp4MediaChunk; 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.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation; import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.parser.Extractor; import com.google.android.exoplayer.parser.Extractor;
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.parser.webm.WebmExtractor; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
/** /**
* An {@link ChunkSource} for DASH streams. * An {@link ChunkSource} for DASH streams.
* <p> * <p>
* This implementation currently supports fMP4 and webm. * This implementation currently supports fMP4, webm, and webvtt.
*/ */
public class DashChunkSource implements ChunkSource { 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 TrackInfo trackInfo;
private final DataSource dataSource; private final DataSource dataSource;
private final FormatEvaluator evaluator; private final FormatEvaluator evaluator;
private final Evaluation evaluation; private final Evaluation evaluation;
private final StringBuilder headerBuilder;
private final long liveEdgeLatencyUs;
private final int maxWidth; private final int maxWidth;
private final int maxHeight; private final int maxHeight;
private final Format[] formats; private final Format[] formats;
private final HashMap<String, Representation> representations; private final HashMap<String, RepresentationHolder> representationHolders;
private final HashMap<String, Extractor> extractors;
private final HashMap<String, DashSegmentIndex> segmentIndexes; private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private final int adaptationSetIndex;
private final int[] representationIndices;
private MediaPresentationDescription currentManifest;
private boolean finishedCurrentManifest;
private boolean lastChunkWasInitialization; 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 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. * @param representations The representations to be considered by the source.
*/ */
public DashChunkSource(DataSource dataSource, FormatEvaluator evaluator, public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator,
Representation... representations) { Representation... representations) {
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<Representation> 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.
* <p>
* 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<MediaPresentationDescription> manifestFetcher,
int adaptationSetIndex, int[] representationIndices, DataSource dataSource,
FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) {
this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices,
dataSource, formatEvaluator, liveEdgeLatencyMs * 1000);
}
private DashChunkSource(ManifestFetcher<MediaPresentationDescription> 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.dataSource = dataSource;
this.evaluator = evaluator; this.evaluator = formatEvaluator;
this.formats = new Format[representations.length]; this.liveEdgeLatencyUs = liveEdgeLatencyUs;
this.extractors = new HashMap<String, Extractor>();
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
this.representations = new HashMap<String, Representation>();
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
representations[0].periodDurationMs * 1000);
this.evaluation = new Evaluation(); 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<String, RepresentationHolder>();
int maxWidth = 0; int maxWidth = 0;
int maxHeight = 0; int maxHeight = 0;
for (int i = 0; i < representations.length; i++) { for (int i = 0; i < representations.length; i++) {
formats[i] = representations[i].format; formats[i] = representations[i].format;
maxWidth = Math.max(formats[i].width, maxWidth); maxWidth = Math.max(formats[i].width, maxWidth);
maxHeight = Math.max(formats[i].height, maxHeight); maxHeight = Math.max(formats[i].height, maxHeight);
Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor()
? new WebmExtractor() : new FragmentedMp4Extractor(); : new FragmentedMp4Extractor();
extractors.put(formats[i].id, extractor); representationHolders.put(formats[i].id,
this.representations.put(formats[i].id, representations[i]); new RepresentationHolder(representations[i], extractor));
DashSegmentIndex segmentIndex = representations[i].getIndex();
if (segmentIndex != null) {
segmentIndexes.put(formats[i].id, segmentIndex);
}
} }
this.maxWidth = maxWidth; this.maxWidth = maxWidth;
this.maxHeight = maxHeight; this.maxHeight = maxHeight;
...@@ -116,21 +219,67 @@ public class DashChunkSource implements ChunkSource { ...@@ -116,21 +219,67 @@ public class DashChunkSource implements ChunkSource {
@Override @Override
public void enable() { public void enable() {
evaluator.enable(); evaluator.enable();
if (manifestFetcher != null) {
manifestFetcher.enable();
}
} }
@Override @Override
public void disable(List<? extends MediaChunk> queue) { public void disable(List<? extends MediaChunk> queue) {
evaluator.disable(); evaluator.disable();
if (manifestFetcher != null) {
manifestFetcher.disable();
}
} }
@Override @Override
public void continueBuffering(long playbackPositionUs) { 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 @Override
public final void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs, public final void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
long playbackPositionUs, ChunkOperationHolder out) { long playbackPositionUs, ChunkOperationHolder out) {
if (fatalError != null) {
out.chunk = null;
return;
}
evaluation.queueSize = queue.size(); evaluation.queueSize = queue.size();
if (evaluation.format == null || !lastChunkWasInitialization) { if (evaluation.format == null || !lastChunkWasInitialization) {
evaluator.evaluate(queue, playbackPositionUs, formats, evaluation); evaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
...@@ -148,17 +297,21 @@ public class DashChunkSource implements ChunkSource { ...@@ -148,17 +297,21 @@ public class DashChunkSource implements ChunkSource {
return; return;
} }
Representation selectedRepresentation = representations.get(selectedFormat.id); RepresentationHolder representationHolder = representationHolders.get(selectedFormat.id);
Extractor extractor = extractors.get(selectedRepresentation.format.id); Representation selectedRepresentation = representationHolder.representation;
DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
Extractor extractor = representationHolder.extractor;
RangedUri pendingInitializationUri = null; RangedUri pendingInitializationUri = null;
RangedUri pendingIndexUri = null; RangedUri pendingIndexUri = null;
if (extractor.getFormat() == null) { if (extractor.getFormat() == null) {
pendingInitializationUri = selectedRepresentation.getInitializationUri(); pendingInitializationUri = selectedRepresentation.getInitializationUri();
} }
if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) { if (segmentIndex == null) {
pendingIndexUri = selectedRepresentation.getIndexUri(); pendingIndexUri = selectedRepresentation.getIndexUri();
} }
if (pendingInitializationUri != null || pendingIndexUri != null) { if (pendingInitializationUri != null || pendingIndexUri != null) {
// We have initialization and/or index requests to make. // We have initialization and/or index requests to make.
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri, Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
...@@ -168,28 +321,48 @@ public class DashChunkSource implements ChunkSource { ...@@ -168,28 +321,48 @@ public class DashChunkSource implements ChunkSource {
return; return;
} }
int nextSegmentNum; int segmentNum;
DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
if (queue.isEmpty()) { if (queue.isEmpty()) {
nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs); if (currentManifest.dynamic) {
seekPositionUs = getLiveSeekPosition();
}
segmentNum = segmentIndex.getSegmentNum(seekPositionUs);
} else { } else {
nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex; segmentNum = queue.get(out.queueSize - 1).nextChunkIndex
- representationHolder.segmentNumShift;
}
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 (nextSegmentNum == -1) { if (segmentNum == -1) {
out.chunk = null; out.chunk = null;
return; return;
} }
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor, Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, segmentNum,
dataSource, nextSegmentNum, evaluation.trigger); evaluation.trigger);
lastChunkWasInitialization = false; lastChunkWasInitialization = false;
out.chunk = nextMediaChunk; out.chunk = nextMediaChunk;
} }
@Override @Override
public IOException getError() { public IOException getError() {
return null; return fatalError != null ? fatalError
: (manifestFetcher != null ? manifestFetcher.getError() : null);
} }
@Override @Override
...@@ -229,22 +402,90 @@ public class DashChunkSource implements ChunkSource { ...@@ -229,22 +402,90 @@ public class DashChunkSource implements ChunkSource {
} }
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
representation.getCacheKey()); representation.getCacheKey());
return new InitializationLoadable(dataSource, dataSpec, trigger, representation.format, return new InitializationLoadable(dataSource, dataSpec, trigger, representation.format,
extractor, expectedExtractorResult, indexAnchor); extractor, expectedExtractorResult, indexAnchor);
} }
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex, private Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource,
Extractor extractor, DataSource dataSource, int segmentNum, int trigger) { int segmentNum, int trigger) {
int lastSegmentNum = segmentIndex.getLastSegmentNum(); Representation representation = representationHolder.representation;
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1; DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
long startTimeUs = segmentIndex.getTimeUs(segmentNum); long startTimeUs = segmentIndex.getTimeUs(segmentNum);
long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1) long endTimeUs = startTimeUs + segmentIndex.getDurationUs(segmentNum);
: startTimeUs + segmentIndex.getDurationUs(segmentNum);
boolean isLastSegment = !currentManifest.dynamic
&& segmentNum == segmentIndex.getLastSegmentNum();
int nextAbsoluteSegmentNum = isLastSegment ? -1
: (representationHolder.segmentNumShift + segmentNum + 1);
RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum); RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
representation.getCacheKey()); representation.getCacheKey());
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, return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs,
endTimeUs, nextSegmentNum, extractor, false, 0); 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<Representation> 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<Representation> 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 { private class InitializationLoadable extends Chunk {
...@@ -272,9 +513,28 @@ public class DashChunkSource implements ChunkSource { ...@@ -272,9 +513,28 @@ public class DashChunkSource implements ChunkSource {
+ expectedExtractorResult + ", got " + result); + expectedExtractorResult + ", got " + result);
} }
if ((result & Extractor.RESULT_READ_INDEX) != 0) { if ((result & Extractor.RESULT_READ_INDEX) != 0) {
segmentIndexes.put(format.id, representationHolders.get(format.id).segmentIndex =
new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor)); 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();
} }
} }
......
...@@ -56,4 +56,21 @@ public class Period { ...@@ -56,4 +56,21 @@ public class Period {
this.adaptationSets = Collections.unmodifiableList(adaptationSets); 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;
}
} }
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.dash.mpd; package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.net.Uri; import android.net.Uri;
...@@ -141,11 +142,12 @@ public abstract class SegmentBase { ...@@ -141,11 +142,12 @@ public abstract class SegmentBase {
public final long getSegmentDurationUs(int sequenceNumber) { public final long getSegmentDurationUs(int sequenceNumber) {
if (segmentTimeline != null) { if (segmentTimeline != null) {
return (segmentTimeline.get(sequenceNumber - startNumber).duration * 1000000) / timescale; long duration = segmentTimeline.get(sequenceNumber - startNumber).duration;
return (duration * C.MICROS_PER_SECOND) / timescale;
} else { } else {
return sequenceNumber == getLastSegmentNum() return sequenceNumber == getLastSegmentNum()
? (periodDurationMs * 1000) - getSegmentTimeUs(sequenceNumber) ? ((periodDurationMs * 1000) - getSegmentTimeUs(sequenceNumber))
: ((duration * 1000000L) / timescale); : ((duration * C.MICROS_PER_SECOND) / timescale);
} }
} }
...@@ -157,7 +159,7 @@ public abstract class SegmentBase { ...@@ -157,7 +159,7 @@ public abstract class SegmentBase {
} else { } else {
unscaledSegmentTime = (sequenceNumber - startNumber) * duration; unscaledSegmentTime = (sequenceNumber - startNumber) * duration;
} }
return Util.scaleLargeTimestamp(unscaledSegmentTime, 1000000, timescale); return Util.scaleLargeTimestamp(unscaledSegmentTime, C.MICROS_PER_SECOND, timescale);
} }
public abstract RangedUri getSegmentUrl(Representation representation, int index); public abstract RangedUri getSegmentUrl(Representation representation, int index);
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.parser.mp4; package com.google.android.exoplayer.parser.mp4;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
...@@ -26,6 +27,7 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream; ...@@ -26,6 +27,7 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.media.MediaCodec; import android.media.MediaCodec;
...@@ -1053,6 +1055,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -1053,6 +1055,7 @@ public final class FragmentedMp4Extractor implements Extractor {
long offset = firstOffset; long offset = firstOffset;
long time = earliestPresentationTime; long time = earliestPresentationTime;
long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
for (int i = 0; i < referenceCount; i++) { for (int i = 0; i < referenceCount; i++) {
int firstInt = atom.readInt(); int firstInt = atom.readInt();
...@@ -1067,10 +1070,10 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -1067,10 +1070,10 @@ public final class FragmentedMp4Extractor implements Extractor {
// Calculate time and duration values such that any rounding errors are consistent. i.e. That // Calculate time and duration values such that any rounding errors are consistent. i.e. That
// timesUs[i] + durationsUs[i] == timesUs[i + 1]. // timesUs[i] + durationsUs[i] == timesUs[i + 1].
timesUs[i] = (time * 1000000L) / timescale; timesUs[i] = timeUs;
long nextTimeUs = ((time + referenceDuration) * 1000000L) / timescale;
durationsUs[i] = nextTimeUs - timesUs[i];
time += referenceDuration; time += referenceDuration;
timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
durationsUs[i] = timeUs - timesUs[i];
atom.skip(4); atom.skip(4);
offset += sizes[i]; offset += sizes[i];
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.smoothstreaming; package com.google.android.exoplayer.smoothstreaming;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
...@@ -31,30 +32,77 @@ import java.util.UUID; ...@@ -31,30 +32,77 @@ import java.util.UUID;
*/ */
public class SmoothStreamingManifest { public class SmoothStreamingManifest {
private static final long MICROS_PER_SECOND = 1000000L; /**
* The client manifest major version.
*/
public final int majorVersion; public final int majorVersion;
/**
* The client manifest minor version.
*/
public final int minorVersion; public final int minorVersion;
public final long timescale;
/**
* The number of fragments in a lookahead, or -1 if the lookahead is unspecified.
*/
public final int lookAheadCount; public final int lookAheadCount;
/**
* True if the manifest describes a live presentation still in progress. False otherwise.
*/
public final boolean isLive; public final boolean isLive;
/**
* Content protection information, or null if the content is not protected.
*/
public final ProtectionElement protectionElement; public final ProtectionElement protectionElement;
/**
* The contained stream elements.
*/
public final StreamElement[] streamElements; public final StreamElement[] streamElements;
/**
* The overall presentation duration of the media in microseconds, or {@link C#UNKNOWN_TIME_US}
* if the duration is unknown.
*/
public final long durationUs; public final long durationUs;
/**
* The length of the trailing window for a live broadcast in microseconds, or
* {@link C#UNKNOWN_TIME_US} if the stream is not live or if the window length is unspecified.
*/
public final long dvrWindowLengthUs; public final long dvrWindowLengthUs;
/**
* @param majorVersion The client manifest major version.
* @param minorVersion The client manifest minor version.
* @param timescale The timescale of the media as the number of units that pass in one second.
* @param duration The overall presentation duration in units of the timescale attribute, or 0
* if the duration is unknown.
* @param dvrWindowLength The length of the trailing window in units of the timescale attribute,
* or 0 if this attribute is unspecified or not applicable.
* @param lookAheadCount The number of fragments in a lookahead, or -1 if this attribute is
* unspecified or not applicable.
* @param isLive True if the manifest describes a live presentation still in progress. False
* otherwise.
* @param protectionElement Content protection information, or null if the content is not
* protected.
* @param streamElements The contained stream elements.
*/
public SmoothStreamingManifest(int majorVersion, int minorVersion, long timescale, long duration, public SmoothStreamingManifest(int majorVersion, int minorVersion, long timescale, long duration,
long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement, long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement,
StreamElement[] streamElements) { StreamElement[] streamElements) {
this.majorVersion = majorVersion; this.majorVersion = majorVersion;
this.minorVersion = minorVersion; this.minorVersion = minorVersion;
this.timescale = timescale;
this.lookAheadCount = lookAheadCount; this.lookAheadCount = lookAheadCount;
this.isLive = isLive; this.isLive = isLive;
this.protectionElement = protectionElement; this.protectionElement = protectionElement;
this.streamElements = streamElements; this.streamElements = streamElements;
dvrWindowLengthUs = Util.scaleLargeTimestamp(dvrWindowLength, MICROS_PER_SECOND, timescale); dvrWindowLengthUs = dvrWindowLength == 0 ? C.UNKNOWN_TIME_US
durationUs = Util.scaleLargeTimestamp(duration, MICROS_PER_SECOND, timescale); : Util.scaleLargeTimestamp(dvrWindowLength, C.MICROS_PER_SECOND, timescale);
durationUs = duration == 0 ? C.UNKNOWN_TIME_US
: Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale);
} }
/** /**
...@@ -176,9 +224,9 @@ public class SmoothStreamingManifest { ...@@ -176,9 +224,9 @@ public class SmoothStreamingManifest {
this.chunkCount = chunkStartTimes.size(); this.chunkCount = chunkStartTimes.size();
this.chunkStartTimes = chunkStartTimes; this.chunkStartTimes = chunkStartTimes;
lastChunkDurationUs = lastChunkDurationUs =
Util.scaleLargeTimestamp(lastChunkDuration, MICROS_PER_SECOND, timescale); Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale);
chunkStartTimesUs = chunkStartTimesUs =
Util.scaleLargeTimestamps(chunkStartTimes, MICROS_PER_SECOND, timescale); Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale);
} }
/** /**
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.text.ttml; package com.google.android.exoplayer.text.ttml;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.text.Subtitle; import com.google.android.exoplayer.text.Subtitle;
import com.google.android.exoplayer.text.SubtitleParser; import com.google.android.exoplayer.text.SubtitleParser;
...@@ -254,7 +255,7 @@ public class TtmlParser implements SubtitleParser { ...@@ -254,7 +255,7 @@ public class TtmlParser implements SubtitleParser {
String subframes = matcher.group(6); String subframes = matcher.group(6);
durationSeconds += (subframes != null) ? durationSeconds += (subframes != null) ?
((double) Long.parseLong(subframes)) / subframeRate / frameRate : 0; ((double) Long.parseLong(subframes)) / subframeRate / frameRate : 0;
return (long) (durationSeconds * 1000000); return (long) (durationSeconds * C.MICROS_PER_SECOND);
} }
matcher = OFFSET_TIME.matcher(time); matcher = OFFSET_TIME.matcher(time);
if (matcher.matches()) { if (matcher.matches()) {
...@@ -274,7 +275,7 @@ public class TtmlParser implements SubtitleParser { ...@@ -274,7 +275,7 @@ public class TtmlParser implements SubtitleParser {
} else if (unit.equals("t")) { } else if (unit.equals("t")) {
offsetSeconds /= tickRate; offsetSeconds /= tickRate;
} }
return (long) (offsetSeconds * 1000000); return (long) (offsetSeconds * C.MICROS_PER_SECOND);
} }
throw new ParserException("Malformed time expression: " + time); throw new ParserException("Malformed time expression: " + time);
} }
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer.util; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer.util;
import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.upstream.Loader.Loadable;
import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.Pair; import android.util.Pair;
...@@ -29,13 +30,26 @@ import java.net.URLConnection; ...@@ -29,13 +30,26 @@ import java.net.URLConnection;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
/** /**
* Performs both single and repeated loads of media manfifests. * Performs both single and repeated loads of media manifests.
* *
* @param <T> The type of manifest. * @param <T> The type of manifest.
*/ */
public class ManifestFetcher<T> implements Loader.Callback { public class ManifestFetcher<T> 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. * Callback for the result of a single load.
* *
* @param <T> The type of manifest. * @param <T> The type of manifest.
...@@ -61,9 +75,12 @@ public class ManifestFetcher<T> implements Loader.Callback { ...@@ -61,9 +75,12 @@ public class ManifestFetcher<T> implements Loader.Callback {
} }
/* package */ final ManifestParser<T> parser; /* package */ final ManifestParser<T> parser;
/* package */ final String manifestUrl;
/* package */ final String contentId; /* package */ final String contentId;
/* package */ final String userAgent; /* package */ final String userAgent;
private final Handler eventHandler;
private final EventListener eventListener;
/* package */ volatile String manifestUrl;
private int enabledCount; private int enabledCount;
private Loader loader; private Loader loader;
...@@ -76,6 +93,11 @@ public class ManifestFetcher<T> implements Loader.Callback { ...@@ -76,6 +93,11 @@ public class ManifestFetcher<T> implements Loader.Callback {
private volatile T manifest; private volatile T manifest;
private volatile long manifestLoadTimestamp; private volatile long manifestLoadTimestamp;
public ManifestFetcher(ManifestParser<T> 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 parser A parser to parse the loaded manifest data.
* @param contentId The content id of the content being loaded. May be null. * @param contentId The content id of the content being loaded. May be null.
...@@ -83,11 +105,22 @@ public class ManifestFetcher<T> implements Loader.Callback { ...@@ -83,11 +105,22 @@ public class ManifestFetcher<T> implements Loader.Callback {
* @param userAgent The User-Agent string that should be used. * @param userAgent The User-Agent string that should be used.
*/ */
public ManifestFetcher(ManifestParser<T> parser, String contentId, String manifestUrl, public ManifestFetcher(ManifestParser<T> parser, String contentId, String manifestUrl,
String userAgent) { String userAgent, Handler eventHandler, EventListener eventListener) {
this.parser = parser; this.parser = parser;
this.contentId = contentId; this.contentId = contentId;
this.manifestUrl = manifestUrl; this.manifestUrl = manifestUrl;
this.userAgent = userAgent; 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<T> implements Loader.Callback { ...@@ -173,6 +206,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
if (!loader.isLoading()) { if (!loader.isLoading()) {
currentLoadable = new ManifestLoadable(); currentLoadable = new ManifestLoadable();
loader.startLoading(currentLoadable, this); loader.startLoading(currentLoadable, this);
notifyManifestRefreshStarted();
} }
} }
...@@ -187,6 +221,8 @@ public class ManifestFetcher<T> implements Loader.Callback { ...@@ -187,6 +221,8 @@ public class ManifestFetcher<T> implements Loader.Callback {
manifestLoadTimestamp = SystemClock.elapsedRealtime(); manifestLoadTimestamp = SystemClock.elapsedRealtime();
loadExceptionCount = 0; loadExceptionCount = 0;
loadException = null; loadException = null;
notifyManifestRefreshed();
} }
@Override @Override
...@@ -204,12 +240,47 @@ public class ManifestFetcher<T> implements Loader.Callback { ...@@ -204,12 +240,47 @@ public class ManifestFetcher<T> implements Loader.Callback {
loadExceptionCount++; loadExceptionCount++;
loadExceptionTimestamp = SystemClock.elapsedRealtime(); loadExceptionTimestamp = SystemClock.elapsedRealtime();
loadException = new IOException(exception); loadException = new IOException(exception);
notifyManifestError(loadException);
} }
private long getRetryDelayMillis(long errorCount) { private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000); 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 class SingleFetchHelper implements Loader.Callback {
private final Looper callbackLooper; private final Looper callbackLooper;
......
...@@ -70,12 +70,14 @@ public class PlayerControl implements MediaPlayerControl { ...@@ -70,12 +70,14 @@ public class PlayerControl implements MediaPlayerControl {
@Override @Override
public int getCurrentPosition() { public int getCurrentPosition() {
return (int) exoPlayer.getCurrentPosition(); return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0
: (int) exoPlayer.getCurrentPosition();
} }
@Override @Override
public int getDuration() { public int getDuration() {
return (int) exoPlayer.getDuration(); return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0
: (int) exoPlayer.getDuration();
} }
@Override @Override
...@@ -95,8 +97,9 @@ public class PlayerControl implements MediaPlayerControl { ...@@ -95,8 +97,9 @@ public class PlayerControl implements MediaPlayerControl {
@Override @Override
public void seekTo(int timeMillis) { public void seekTo(int timeMillis) {
// MediaController arrow keys generate unbounded values. long seekPosition = exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0
exoPlayer.seekTo(Math.min(Math.max(0, timeMillis), getDuration())); : Math.min(Math.max(0, timeMillis), getDuration());
exoPlayer.seekTo(seekPosition);
} }
} }
...@@ -399,4 +399,22 @@ public final class Util { ...@@ -399,4 +399,22 @@ public final class Util {
return scaledTimestamps; 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<Integer> 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;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment