mirror of
https://github.com/androidx/media.git
synced 2025-05-10 00:59:51 +08:00
Implement DASH Live.
Note: This adds support for the majority of DASH live streams, however we do not yet correctly support live streams that rely on UtcTimingElements in their manifests. Issue: #52
This commit is contained in:
parent
6652f864bd
commit
4efc0abde9
@ -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;
|
||||||
|
|
||||||
|
@ -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",
|
||||||
@ -66,13 +66,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),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,12 +81,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[] {
|
||||||
@ -103,32 +103,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[] MISC = new Sample[] {
|
public static final Sample[] MISC = new Sample[] {
|
||||||
|
@ -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;
|
||||||
@ -172,8 +172,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);
|
||||||
default:
|
default:
|
||||||
return new DefaultRendererBuilder(this, contentUri, debugTextView);
|
return new DefaultRendererBuilder(this, contentUri, debugTextView);
|
||||||
|
@ -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;
|
|||||||
|
|
||||||
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,
|
|||||||
|
|
||||||
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,
|
|||||||
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,
|
|||||||
|
|
||||||
@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,
|
|||||||
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.
|
filterHdContent = !drmSessionManagerData.second;
|
||||||
videoRepresentations = getSdRepresentations(videoRepresentations);
|
|
||||||
}
|
|
||||||
} 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)) {
|
||||||
} else {
|
// Filtering HD content
|
||||||
throw new IllegalStateException("Unexpected mime type: " + mimeType);
|
} else if (format.width * format.height > maxDecodableFrameSize) {
|
||||||
|
// Filtering stream that device cannot play
|
||||||
|
} else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4)
|
||||||
|
&& !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) {
|
||||||
|
// Filtering unsupported mime type
|
||||||
|
} else {
|
||||||
|
videoRepresentationIndexList.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the video renderer.
|
||||||
|
final MediaCodecVideoTrackRenderer videoRenderer;
|
||||||
|
final TrackRenderer debugRenderer;
|
||||||
|
if (videoRepresentationIndexList.isEmpty()) {
|
||||||
|
videoRenderer = null;
|
||||||
|
debugRenderer = null;
|
||||||
|
} else {
|
||||||
|
int[] videoRepresentationIndices = Util.toArray(videoRepresentationIndexList);
|
||||||
|
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
|
||||||
|
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex,
|
||||||
|
videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter),
|
||||||
|
LIVE_EDGE_LATENCY_MS);
|
||||||
|
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||||
|
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
|
||||||
|
DemoPlayer.TYPE_VIDEO);
|
||||||
|
videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true,
|
||||||
|
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50);
|
||||||
|
debugRenderer = debugTextView != null
|
||||||
|
? new DebugTrackRenderer(debugTextView, videoRenderer, videoSampleSource) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the audio chunk sources.
|
||||||
|
int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO);
|
||||||
|
AdaptationSet audioAdaptationSet = period.adaptationSets.get(audioAdaptationSetIndex);
|
||||||
|
DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter);
|
||||||
|
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
|
||||||
|
List<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));
|
||||||
}
|
}
|
||||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
|
||||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
|
|
||||||
DemoPlayer.TYPE_VIDEO);
|
|
||||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
|
|
||||||
drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null,
|
|
||||||
mainHandler, player, 50);
|
|
||||||
|
|
||||||
// Build the audio renderer.
|
// 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,
|
|||||||
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,
|
|||||||
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,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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,
|
|||||||
}
|
}
|
||||||
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;
|
|||||||
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;
|
|||||||
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;
|
|||||||
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;
|
|||||||
|
|
||||||
@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;
|
} else {
|
||||||
if (format.width * format.height <= maxDecodableFrameSize) {
|
videoRepresentationIndexList.add(i);
|
||||||
videoRepresentationsList.add(representation);
|
|
||||||
} else {
|
|
||||||
// The device isn't capable of playing this stream.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()];
|
|
||||||
videoRepresentationsList.toArray(videoRepresentations);
|
|
||||||
|
|
||||||
// Build the video renderer.
|
// Build the video renderer.
|
||||||
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
|
final MediaCodecVideoTrackRenderer videoRenderer;
|
||||||
ChunkSource videoChunkSource = new DashChunkSource(videoDataSource,
|
if (videoRepresentationIndexList.isEmpty()) {
|
||||||
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
|
videoRenderer = null;
|
||||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
} else {
|
||||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
|
int[] videoRepresentationIndices = Util.toArray(videoRepresentationIndexList);
|
||||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
|
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
|
||||||
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
|
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex,
|
||||||
|
videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter),
|
||||||
|
LIVE_EDGE_LATENCY_MS);
|
||||||
|
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||||
|
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
|
||||||
|
videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
|
||||||
|
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
|
||||||
|
}
|
||||||
|
|
||||||
// Build the audio renderer.
|
// 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(
|
@ -61,10 +61,6 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
|
|||||||
|
|
||||||
private static final String TAG = "PlayerActivity";
|
private static final String TAG = "PlayerActivity";
|
||||||
|
|
||||||
public static final int TYPE_DASH_VOD = 0;
|
|
||||||
public static final int TYPE_SS_VOD = 1;
|
|
||||||
public static final int TYPE_OTHER = 2;
|
|
||||||
|
|
||||||
private MediaController mediaController;
|
private MediaController mediaController;
|
||||||
private Handler mainHandler;
|
private Handler mainHandler;
|
||||||
private View shutterView;
|
private View shutterView;
|
||||||
@ -90,7 +86,7 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
|
|||||||
|
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
contentUri = intent.getData();
|
contentUri = intent.getData();
|
||||||
contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, TYPE_OTHER);
|
contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, DemoUtil.TYPE_OTHER);
|
||||||
contentId = intent.getStringExtra(DemoUtil.CONTENT_ID_EXTRA);
|
contentId = intent.getStringExtra(DemoUtil.CONTENT_ID_EXTRA);
|
||||||
|
|
||||||
mainHandler = new Handler(getMainLooper());
|
mainHandler = new Handler(getMainLooper());
|
||||||
@ -165,11 +161,11 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
|
|||||||
private RendererBuilder getRendererBuilder() {
|
private RendererBuilder getRendererBuilder() {
|
||||||
String userAgent = DemoUtil.getUserAgent(this);
|
String userAgent = DemoUtil.getUserAgent(this);
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case TYPE_SS_VOD:
|
case DemoUtil.TYPE_SS:
|
||||||
return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(),
|
return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(),
|
||||||
contentId);
|
contentId);
|
||||||
case TYPE_DASH_VOD:
|
case DemoUtil.TYPE_DASH:
|
||||||
return new DashVodRendererBuilder(this, userAgent, contentUri.toString(), contentId);
|
return new DashRendererBuilder(this, userAgent, contentUri.toString(), contentId);
|
||||||
default:
|
default:
|
||||||
return new DefaultRendererBuilder(this, contentUri);
|
return new DefaultRendererBuilder(this, contentUri);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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);
|
||||||
|
@ -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 {
|
|||||||
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,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,76 +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) {
|
||||||
long periodDurationUs = (representations[0].periodDurationMs == -1)
|
this(buildManifest(Arrays.asList(representations)), 0, null, dataSource, formatEvaluator);
|
||||||
? -1 : representations[0].periodDurationMs * 1000;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, periodDurationUs);
|
|
||||||
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;
|
||||||
@ -118,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);
|
||||||
@ -150,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,
|
||||||
@ -170,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 (nextSegmentNum == -1) {
|
if (currentManifest.dynamic) {
|
||||||
|
if (segmentNum < segmentIndex.getFirstSegmentNum()) {
|
||||||
|
// This is before the first chunk in the current manifest.
|
||||||
|
fatalError = new BehindLiveWindowException();
|
||||||
|
return;
|
||||||
|
} else if (segmentNum > segmentIndex.getLastSegmentNum()) {
|
||||||
|
// This is beyond the last chunk in the current manifest.
|
||||||
|
finishedCurrentManifest = true;
|
||||||
|
return;
|
||||||
|
} else if (segmentNum == segmentIndex.getLastSegmentNum()) {
|
||||||
|
// This is the last chunk in the current manifest. Mark the manifest as being finished,
|
||||||
|
// but continue to return the final chunk.
|
||||||
|
finishedCurrentManifest = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segmentNum == -1) {
|
||||||
out.chunk = null;
|
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
|
||||||
@ -231,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());
|
||||||
return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs,
|
|
||||||
endTimeUs, nextSegmentNum, extractor, false, 0);
|
long presentationTimeOffsetUs = representation.presentationTimeOffsetMs * 1000;
|
||||||
|
if (representation.format.mimeType.equals(MimeTypes.TEXT_VTT)) {
|
||||||
|
if (representationHolder.vttHeaderOffsetUs != presentationTimeOffsetUs) {
|
||||||
|
// Update the VTT header.
|
||||||
|
headerBuilder.setLength(0);
|
||||||
|
headerBuilder.append(WebvttParser.EXO_HEADER).append("=")
|
||||||
|
.append(WebvttParser.OFFSET).append(presentationTimeOffsetUs).append("\n");
|
||||||
|
representationHolder.vttHeader = headerBuilder.toString().getBytes();
|
||||||
|
representationHolder.vttHeaderOffsetUs = presentationTimeOffsetUs;
|
||||||
|
}
|
||||||
|
return new SingleSampleMediaChunk(dataSource, dataSpec, representation.format, 0,
|
||||||
|
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader);
|
||||||
|
} else {
|
||||||
|
return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs,
|
||||||
|
endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, false,
|
||||||
|
presentationTimeOffsetUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For live playbacks, determines the seek position that snaps playback to be
|
||||||
|
* {@link #liveEdgeLatencyUs} behind the live edge of the current manifest
|
||||||
|
*
|
||||||
|
* @return The seek position in microseconds.
|
||||||
|
*/
|
||||||
|
private long getLiveSeekPosition() {
|
||||||
|
long liveEdgeTimestampUs = Long.MIN_VALUE;
|
||||||
|
for (RepresentationHolder representationHolder : representationHolders.values()) {
|
||||||
|
DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
|
||||||
|
int lastSegmentNum = segmentIndex.getLastSegmentNum();
|
||||||
|
long indexLiveEdgeTimestampUs = segmentIndex.getTimeUs(lastSegmentNum)
|
||||||
|
+ segmentIndex.getDurationUs(lastSegmentNum);
|
||||||
|
liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, indexLiveEdgeTimestampUs);
|
||||||
|
}
|
||||||
|
return liveEdgeTimestampUs - liveEdgeLatencyUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Representation[] getFilteredRepresentations(MediaPresentationDescription manifest,
|
||||||
|
int adaptationSetIndex, int[] representationIndices) {
|
||||||
|
List<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 {
|
||||||
@ -274,11 +513,30 @@ 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 {
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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,12 +30,25 @@ 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.
|
||||||
*
|
*
|
||||||
@ -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 {
|
|||||||
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 {
|
|||||||
* @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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
@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 {
|
|||||||
|
|
||||||
@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 {
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user