mirror of
https://github.com/androidx/media.git
synced 2025-05-17 04:29:55 +08:00
Merge branch 'dev' into dev-hls
This commit is contained in:
commit
4c29eb58f1
@ -18,7 +18,6 @@ package com.google.android.exoplayer.demo;
|
|||||||
import com.google.android.exoplayer.demo.Samples.Sample;
|
import com.google.android.exoplayer.demo.Samples.Sample;
|
||||||
import com.google.android.exoplayer.demo.full.FullPlayerActivity;
|
import com.google.android.exoplayer.demo.full.FullPlayerActivity;
|
||||||
import com.google.android.exoplayer.demo.simple.SimplePlayerActivity;
|
import com.google.android.exoplayer.demo.simple.SimplePlayerActivity;
|
||||||
import com.google.android.exoplayer.util.Util;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -33,7 +32,6 @@ import android.widget.AdapterView.OnItemClickListener;
|
|||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An activity for selecting from a number of samples.
|
* An activity for selecting from a number of samples.
|
||||||
@ -78,11 +76,6 @@ public class SampleChooserActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onSampleSelected(Sample sample) {
|
private void onSampleSelected(Sample sample) {
|
||||||
if (Util.SDK_INT < 18 && sample.isEncypted) {
|
|
||||||
Toast.makeText(getApplicationContext(), R.string.drm_not_supported, Toast.LENGTH_SHORT)
|
|
||||||
.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Class<?> playerActivityClass = sample.fullPlayer ? FullPlayerActivity.class
|
Class<?> playerActivityClass = sample.fullPlayer ? FullPlayerActivity.class
|
||||||
: SimplePlayerActivity.class;
|
: SimplePlayerActivity.class;
|
||||||
Intent mpdIntent = new Intent(this, playerActivityClass)
|
Intent mpdIntent = new Intent(this, playerActivityClass)
|
||||||
|
@ -26,16 +26,13 @@ package com.google.android.exoplayer.demo;
|
|||||||
public final String contentId;
|
public final String contentId;
|
||||||
public final String uri;
|
public final String uri;
|
||||||
public final int type;
|
public final int type;
|
||||||
public final boolean isEncypted;
|
|
||||||
public final boolean fullPlayer;
|
public final boolean fullPlayer;
|
||||||
|
|
||||||
public Sample(String name, String contentId, String uri, int type, boolean isEncrypted,
|
public Sample(String name, String contentId, String uri, int type, boolean fullPlayer) {
|
||||||
boolean fullPlayer) {
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.contentId = contentId;
|
this.contentId = contentId;
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.isEncypted = isEncrypted;
|
|
||||||
this.fullPlayer = fullPlayer;
|
this.fullPlayer = fullPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,22 +43,20 @@ 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, false,
|
+ "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, 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, false,
|
+ "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, 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",
|
||||||
DemoUtil.TYPE_SS, false, false),
|
DemoUtil.TYPE_SS, false),
|
||||||
new Sample("Apple master playlist (HLS)", "uid:hls:applemaster",
|
new Sample("Apple master playlist (HLS)", "uid:hls:applemaster",
|
||||||
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/"
|
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/"
|
||||||
+ "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS, false, false),
|
+ "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS, false),
|
||||||
new Sample("Dizzy (Misc)", "uid:misc:dizzy",
|
new Sample("Dizzy (Misc)", "uid:misc:dizzy",
|
||||||
"http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false, false),
|
"http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Sample[] YOUTUBE_DASH_MP4 = new Sample[] {
|
public static final Sample[] YOUTUBE_DASH_MP4 = new Sample[] {
|
||||||
@ -69,14 +64,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,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, false,
|
+ "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, 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, false,
|
+ "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, true),
|
||||||
true),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Sample[] YOUTUBE_DASH_WEBM = new Sample[] {
|
public static final Sample[] YOUTUBE_DASH_WEBM = new Sample[] {
|
||||||
@ -84,21 +77,21 @@ 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, false, true),
|
+ "446E9C38E47E3EDAF39E0163C390FF83A7944918&key=ik0", DemoUtil.TYPE_DASH, 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, false, true),
|
+ "C0358CE1E335417D9A8D88FF192F0D5D8F6DA1B6&key=ik0", DemoUtil.TYPE_DASH, true),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Sample[] SMOOTHSTREAMING = new Sample[] {
|
public static final Sample[] SMOOTHSTREAMING = new Sample[] {
|
||||||
new Sample("Super speed", "uid:ss:superspeed",
|
new Sample("Super speed", "uid:ss:superspeed",
|
||||||
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism",
|
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism",
|
||||||
DemoUtil.TYPE_SS, false, true),
|
DemoUtil.TYPE_SS, true),
|
||||||
new Sample("Super speed (PlayReady)", "uid:ss:pr:superspeed",
|
new Sample("Super speed (PlayReady)", "uid:ss:pr:superspeed",
|
||||||
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
|
"http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
|
||||||
DemoUtil.TYPE_SS, true, true),
|
DemoUtil.TYPE_SS, true),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Sample[] WIDEVINE_GTS = new Sample[] {
|
public static final Sample[] WIDEVINE_GTS = new Sample[] {
|
||||||
@ -106,54 +99,54 @@ 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, true, true),
|
+ "0BB075C396FFDDC97E526E8F77DC26FF9667D0D6&key=ik0", DemoUtil.TYPE_DASH, 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, true, true),
|
+ "5B41E2EB411F797097A359D6671D2CDE26272373&key=ik0", DemoUtil.TYPE_DASH, 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, true, true),
|
+ "B09589A34FA23527EFC1552907754BB8033870BD&key=ik0", DemoUtil.TYPE_DASH, 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, true, true),
|
+ "A1CA51EC40A1C1039BA800C41500DD448C03EEDA&key=ik0", DemoUtil.TYPE_DASH, 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, true, true),
|
+ "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH, 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, true, true),
|
+ "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH, true),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Sample[] HLS = new Sample[] {
|
public static final Sample[] HLS = new Sample[] {
|
||||||
new Sample("Apple master playlist", "uid:hls:applemaster",
|
new Sample("Apple master playlist", "uid:hls:applemaster",
|
||||||
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/"
|
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/"
|
||||||
+ "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS, false, true),
|
+ "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS, true),
|
||||||
new Sample("Apple master playlist advanced", "uid:hls:applemasteradvanced",
|
new Sample("Apple master playlist advanced", "uid:hls:applemasteradvanced",
|
||||||
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/"
|
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/"
|
||||||
+ "bipbop_16x9_variant.m3u8", DemoUtil.TYPE_HLS, false, true),
|
+ "bipbop_16x9_variant.m3u8", DemoUtil.TYPE_HLS, true),
|
||||||
new Sample("Apple single media playlist", "uid:hls:applesinglemedia",
|
new Sample("Apple single media playlist", "uid:hls:applesinglemedia",
|
||||||
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/"
|
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/"
|
||||||
+ "prog_index.m3u8", DemoUtil.TYPE_HLS, false, true),
|
+ "prog_index.m3u8", DemoUtil.TYPE_HLS, true),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Sample[] MISC = new Sample[] {
|
public static final Sample[] MISC = new Sample[] {
|
||||||
new Sample("Dizzy", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4",
|
new Sample("Dizzy", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4",
|
||||||
DemoUtil.TYPE_OTHER, false, true),
|
DemoUtil.TYPE_OTHER, true),
|
||||||
new Sample("Dizzy (https->http redirect)", "uid:misc:dizzy2", "https://goo.gl/MtUDEj",
|
new Sample("Dizzy (https->http redirect)", "uid:misc:dizzy2", "https://goo.gl/MtUDEj",
|
||||||
DemoUtil.TYPE_OTHER, false, true),
|
DemoUtil.TYPE_OTHER, true),
|
||||||
new Sample("Apple AAC 10s", "uid:misc:appleaacseg", "https://devimages.apple.com.edgekey.net/"
|
new Sample("Apple AAC 10s", "uid:misc:appleaacseg", "https://devimages.apple.com.edgekey.net/"
|
||||||
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
|
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
|
||||||
DemoUtil.TYPE_OTHER, false, true),
|
DemoUtil.TYPE_OTHER, true),
|
||||||
};
|
};
|
||||||
|
|
||||||
private Samples() {}
|
private Samples() {}
|
||||||
|
@ -17,6 +17,8 @@ package com.google.android.exoplayer.demo.full;
|
|||||||
|
|
||||||
import com.google.android.exoplayer.ExoPlayer;
|
import com.google.android.exoplayer.ExoPlayer;
|
||||||
import com.google.android.exoplayer.VideoSurfaceView;
|
import com.google.android.exoplayer.VideoSurfaceView;
|
||||||
|
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||||
|
import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
|
||||||
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.DashRendererBuilder;
|
import com.google.android.exoplayer.demo.full.player.DashRendererBuilder;
|
||||||
@ -25,6 +27,7 @@ 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;
|
||||||
import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder;
|
import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder;
|
import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder;
|
||||||
|
import com.google.android.exoplayer.demo.full.player.UnsupportedDrmException;
|
||||||
import com.google.android.exoplayer.metadata.TxxxMetadata;
|
import com.google.android.exoplayer.metadata.TxxxMetadata;
|
||||||
import com.google.android.exoplayer.text.CaptionStyleCompat;
|
import com.google.android.exoplayer.text.CaptionStyleCompat;
|
||||||
import com.google.android.exoplayer.text.SubtitleView;
|
import com.google.android.exoplayer.text.SubtitleView;
|
||||||
@ -55,6 +58,7 @@ import android.widget.MediaController;
|
|||||||
import android.widget.PopupMenu;
|
import android.widget.PopupMenu;
|
||||||
import android.widget.PopupMenu.OnMenuItemClickListener;
|
import android.widget.PopupMenu.OnMenuItemClickListener;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -62,7 +66,8 @@ import java.util.Map;
|
|||||||
* An activity that plays media using {@link DemoPlayer}.
|
* An activity that plays media using {@link DemoPlayer}.
|
||||||
*/
|
*/
|
||||||
public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener,
|
public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener,
|
||||||
DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.Id3MetadataListener {
|
DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.Id3MetadataListener,
|
||||||
|
AudioCapabilitiesReceiver.Listener {
|
||||||
|
|
||||||
private static final String TAG = "FullPlayerActivity";
|
private static final String TAG = "FullPlayerActivity";
|
||||||
|
|
||||||
@ -94,6 +99,9 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
|
|||||||
private int contentType;
|
private int contentType;
|
||||||
private String contentId;
|
private String contentId;
|
||||||
|
|
||||||
|
private AudioCapabilitiesReceiver audioCapabilitiesReceiver;
|
||||||
|
private AudioCapabilities audioCapabilities;
|
||||||
|
|
||||||
// Activity lifecycle
|
// Activity lifecycle
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -117,6 +125,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this);
|
||||||
|
|
||||||
shutterView = findViewById(R.id.shutter);
|
shutterView = findViewById(R.id.shutter);
|
||||||
debugRootView = findViewById(R.id.controls_root);
|
debugRootView = findViewById(R.id.controls_root);
|
||||||
|
|
||||||
@ -142,7 +152,9 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
|
|||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
configureSubtitleView();
|
configureSubtitleView();
|
||||||
preparePlayer();
|
|
||||||
|
// The player will be prepared on receiving audio capabilities.
|
||||||
|
audioCapabilitiesReceiver.register();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -153,6 +165,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
|
|||||||
} else {
|
} else {
|
||||||
player.blockingClearSurface();
|
player.blockingClearSurface();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audioCapabilitiesReceiver.unregister();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -171,6 +185,17 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AudioCapabilitiesReceiver.Listener methods
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
|
||||||
|
this.audioCapabilities = audioCapabilities;
|
||||||
|
releasePlayer();
|
||||||
|
|
||||||
|
autoPlay = true;
|
||||||
|
preparePlayer();
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods
|
// Internal methods
|
||||||
|
|
||||||
private RendererBuilder getRendererBuilder() {
|
private RendererBuilder getRendererBuilder() {
|
||||||
@ -181,7 +206,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
|
|||||||
new SmoothStreamingTestMediaDrmCallback(), debugTextView);
|
new SmoothStreamingTestMediaDrmCallback(), debugTextView);
|
||||||
case DemoUtil.TYPE_DASH:
|
case DemoUtil.TYPE_DASH:
|
||||||
return new DashRendererBuilder(userAgent, contentUri.toString(), contentId,
|
return new DashRendererBuilder(userAgent, contentUri.toString(), contentId,
|
||||||
new WidevineTestMediaDrmCallback(contentId), debugTextView);
|
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
|
||||||
case DemoUtil.TYPE_HLS:
|
case DemoUtil.TYPE_HLS:
|
||||||
return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId);
|
return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId);
|
||||||
default:
|
default:
|
||||||
@ -266,6 +291,16 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(Exception e) {
|
public void onError(Exception e) {
|
||||||
|
if (e instanceof UnsupportedDrmException) {
|
||||||
|
// Special case DRM failures.
|
||||||
|
UnsupportedDrmException unsupportedDrmException = (UnsupportedDrmException) e;
|
||||||
|
int stringId = unsupportedDrmException.reason == UnsupportedDrmException.REASON_NO_DRM
|
||||||
|
? R.string.drm_error_not_supported
|
||||||
|
: unsupportedDrmException.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
||||||
|
? R.string.drm_error_unsupported_scheme
|
||||||
|
: R.string.drm_error_unknown;
|
||||||
|
Toast.makeText(getApplicationContext(), stringId, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
playerNeedsPrepare = true;
|
playerNeedsPrepare = true;
|
||||||
updateButtonVisibilities();
|
updateButtonVisibilities();
|
||||||
showControls();
|
showControls();
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.demo.full.player;
|
package com.google.android.exoplayer.demo.full.player;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.Ac3PassthroughAudioTrackRenderer;
|
||||||
import com.google.android.exoplayer.DefaultLoadControl;
|
import com.google.android.exoplayer.DefaultLoadControl;
|
||||||
import com.google.android.exoplayer.LoadControl;
|
import com.google.android.exoplayer.LoadControl;
|
||||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||||
@ -22,6 +23,7 @@ import com.google.android.exoplayer.MediaCodecUtil;
|
|||||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||||
import com.google.android.exoplayer.SampleSource;
|
import com.google.android.exoplayer.SampleSource;
|
||||||
import com.google.android.exoplayer.TrackRenderer;
|
import com.google.android.exoplayer.TrackRenderer;
|
||||||
|
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||||
import com.google.android.exoplayer.chunk.ChunkSource;
|
import com.google.android.exoplayer.chunk.ChunkSource;
|
||||||
import com.google.android.exoplayer.chunk.Format;
|
import com.google.android.exoplayer.chunk.Format;
|
||||||
@ -41,6 +43,7 @@ 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.TextTrackRenderer;
|
||||||
|
import com.google.android.exoplayer.text.ttml.TtmlParser;
|
||||||
import com.google.android.exoplayer.text.webvtt.WebvttParser;
|
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;
|
||||||
@ -83,18 +86,20 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
private final String contentId;
|
private final String contentId;
|
||||||
private final MediaDrmCallback drmCallback;
|
private final MediaDrmCallback drmCallback;
|
||||||
private final TextView debugTextView;
|
private final TextView debugTextView;
|
||||||
|
private final AudioCapabilities audioCapabilities;
|
||||||
|
|
||||||
private DemoPlayer player;
|
private DemoPlayer player;
|
||||||
private RendererBuilderCallback callback;
|
private RendererBuilderCallback callback;
|
||||||
private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
|
private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
|
||||||
|
|
||||||
public DashRendererBuilder(String userAgent, String url, String contentId,
|
public DashRendererBuilder(String userAgent, String url, String contentId,
|
||||||
MediaDrmCallback drmCallback, TextView debugTextView) {
|
MediaDrmCallback drmCallback, TextView debugTextView, AudioCapabilities audioCapabilities) {
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.contentId = contentId;
|
this.contentId = contentId;
|
||||||
this.drmCallback = drmCallback;
|
this.drmCallback = drmCallback;
|
||||||
this.debugTextView = debugTextView;
|
this.debugTextView = debugTextView;
|
||||||
|
this.audioCapabilities = audioCapabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -119,17 +124,33 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
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);
|
||||||
|
|
||||||
|
boolean hasContentProtection = false;
|
||||||
int videoAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_VIDEO);
|
int videoAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_VIDEO);
|
||||||
AdaptationSet videoAdaptationSet = period.adaptationSets.get(videoAdaptationSetIndex);
|
int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO);
|
||||||
|
AdaptationSet videoAdaptationSet = null;
|
||||||
|
AdaptationSet audioAdaptationSet = null;
|
||||||
|
if (videoAdaptationSetIndex != -1) {
|
||||||
|
videoAdaptationSet = period.adaptationSets.get(videoAdaptationSetIndex);
|
||||||
|
hasContentProtection |= videoAdaptationSet.hasContentProtection();
|
||||||
|
}
|
||||||
|
if (audioAdaptationSetIndex != -1) {
|
||||||
|
audioAdaptationSet = period.adaptationSets.get(audioAdaptationSetIndex);
|
||||||
|
hasContentProtection |= audioAdaptationSet.hasContentProtection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail if we have neither video or audio.
|
||||||
|
if (videoAdaptationSet == null && audioAdaptationSet == null) {
|
||||||
|
callback.onRenderersError(new IllegalStateException("No video or audio adaptation sets"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check drm support if necessary.
|
// Check drm support if necessary.
|
||||||
boolean hasContentProtection = videoAdaptationSet.hasContentProtection();
|
|
||||||
boolean filterHdContent = false;
|
boolean filterHdContent = false;
|
||||||
DrmSessionManager drmSessionManager = null;
|
DrmSessionManager drmSessionManager = null;
|
||||||
if (hasContentProtection) {
|
if (hasContentProtection) {
|
||||||
if (Util.SDK_INT < 18) {
|
if (Util.SDK_INT < 18) {
|
||||||
callback.onRenderersError(new UnsupportedOperationException(
|
callback.onRenderersError(
|
||||||
"Protected content not supported on API level " + Util.SDK_INT));
|
new UnsupportedDrmException(UnsupportedDrmException.REASON_NO_DRM));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -137,28 +158,35 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
V18Compat.getDrmSessionManagerData(player, drmCallback);
|
V18Compat.getDrmSessionManagerData(player, drmCallback);
|
||||||
drmSessionManager = drmSessionManagerData.first;
|
drmSessionManager = drmSessionManagerData.first;
|
||||||
// HD streams require L1 security.
|
// HD streams require L1 security.
|
||||||
filterHdContent = !drmSessionManagerData.second;
|
filterHdContent = videoAdaptationSet != null && videoAdaptationSet.hasContentProtection()
|
||||||
|
&& !drmSessionManagerData.second;
|
||||||
|
} catch (UnsupportedSchemeException e) {
|
||||||
|
callback.onRenderersError(
|
||||||
|
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
callback.onRenderersError(e);
|
callback.onRenderersError(
|
||||||
|
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNKNOWN, e));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine which video representations we should use for playback.
|
// Determine which video representations we should use for playback.
|
||||||
int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
|
|
||||||
List<Representation> videoRepresentations = videoAdaptationSet.representations;
|
|
||||||
ArrayList<Integer> videoRepresentationIndexList = new ArrayList<Integer>();
|
ArrayList<Integer> videoRepresentationIndexList = new ArrayList<Integer>();
|
||||||
for (int i = 0; i < videoRepresentations.size(); i++) {
|
if (videoAdaptationSet != null) {
|
||||||
Format format = videoRepresentations.get(i).format;
|
int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
|
||||||
if (filterHdContent && (format.width >= 1280 || format.height >= 720)) {
|
List<Representation> videoRepresentations = videoAdaptationSet.representations;
|
||||||
// Filtering HD content
|
for (int i = 0; i < videoRepresentations.size(); i++) {
|
||||||
} else if (format.width * format.height > maxDecodableFrameSize) {
|
Format format = videoRepresentations.get(i).format;
|
||||||
// Filtering stream that device cannot play
|
if (filterHdContent && (format.width >= 1280 || format.height >= 720)) {
|
||||||
} else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4)
|
// Filtering HD content
|
||||||
&& !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) {
|
} else if (format.width * format.height > maxDecodableFrameSize) {
|
||||||
// Filtering unsupported mime type
|
// Filtering stream that device cannot play
|
||||||
} else {
|
} else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4)
|
||||||
videoRepresentationIndexList.add(i);
|
&& !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) {
|
||||||
|
// Filtering unsupported mime type
|
||||||
|
} else {
|
||||||
|
videoRepresentationIndexList.add(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,19 +212,33 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build the audio chunk sources.
|
// Build the audio chunk sources.
|
||||||
int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO);
|
boolean haveAc3Tracks = false;
|
||||||
AdaptationSet audioAdaptationSet = period.adaptationSets.get(audioAdaptationSetIndex);
|
|
||||||
DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter);
|
|
||||||
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
|
|
||||||
List<ChunkSource> audioChunkSourceList = new ArrayList<ChunkSource>();
|
List<ChunkSource> audioChunkSourceList = new ArrayList<ChunkSource>();
|
||||||
List<String> audioTrackNameList = new ArrayList<String>();
|
List<String> audioTrackNameList = new ArrayList<String>();
|
||||||
List<Representation> audioRepresentations = audioAdaptationSet.representations;
|
if (audioAdaptationSet != null) {
|
||||||
for (int i = 0; i < audioRepresentations.size(); i++) {
|
DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter);
|
||||||
Format format = audioRepresentations.get(i).format;
|
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
|
||||||
audioTrackNameList.add(format.id + " (" + format.numChannels + "ch, " +
|
List<Representation> audioRepresentations = audioAdaptationSet.representations;
|
||||||
format.audioSamplingRate + "Hz)");
|
for (int i = 0; i < audioRepresentations.size(); i++) {
|
||||||
audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex,
|
Format format = audioRepresentations.get(i).format;
|
||||||
new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS));
|
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));
|
||||||
|
haveAc3Tracks |= format.mimeType.equals(MimeTypes.AUDIO_AC3)
|
||||||
|
|| format.mimeType.equals(MimeTypes.AUDIO_EC3);
|
||||||
|
}
|
||||||
|
// Filter out non-AC-3 tracks if there is an AC-3 track, to avoid having to switch renderers.
|
||||||
|
if (haveAc3Tracks) {
|
||||||
|
for (int i = audioRepresentations.size() - 1; i >= 0; i--) {
|
||||||
|
Format format = audioRepresentations.get(i).format;
|
||||||
|
if (!format.mimeType.equals(MimeTypes.AUDIO_AC3)
|
||||||
|
&& !format.mimeType.equals(MimeTypes.AUDIO_EC3)) {
|
||||||
|
audioTrackNameList.remove(i);
|
||||||
|
audioChunkSourceList.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the audio renderer.
|
// Build the audio renderer.
|
||||||
@ -214,8 +256,16 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
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);
|
||||||
audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true,
|
// TODO: There needs to be some logic to filter out non-AC3 tracks when selecting to use AC3.
|
||||||
mainHandler, player);
|
boolean useAc3Passthrough = haveAc3Tracks && audioCapabilities != null
|
||||||
|
&& (audioCapabilities.supportsAc3() || audioCapabilities.supportsEAc3());
|
||||||
|
if (useAc3Passthrough) {
|
||||||
|
audioRenderer =
|
||||||
|
new Ac3PassthroughAudioTrackRenderer(audioSampleSource, mainHandler, player);
|
||||||
|
} else {
|
||||||
|
audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true,
|
||||||
|
mainHandler, player);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the text chunk sources.
|
// Build the text chunk sources.
|
||||||
@ -251,8 +301,8 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
SampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
SampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
||||||
TEXT_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(textSampleSource, new WebvttParser(), player,
|
textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper(),
|
||||||
mainHandler.getLooper());
|
new TtmlParser(), new WebvttParser());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke the callback.
|
// Invoke the callback.
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.demo.full.player;
|
package com.google.android.exoplayer.demo.full.player;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.Ac3PassthroughAudioTrackRenderer;
|
||||||
import com.google.android.exoplayer.DummyTrackRenderer;
|
import com.google.android.exoplayer.DummyTrackRenderer;
|
||||||
import com.google.android.exoplayer.ExoPlaybackException;
|
import com.google.android.exoplayer.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer.ExoPlayer;
|
import com.google.android.exoplayer.ExoPlayer;
|
||||||
@ -49,8 +50,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|||||||
*/
|
*/
|
||||||
public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
|
public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
|
||||||
DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener,
|
DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener,
|
||||||
MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer,
|
MediaCodecAudioTrackRenderer.EventListener, Ac3PassthroughAudioTrackRenderer.EventListener,
|
||||||
StreamingDrmSessionManager.EventListener {
|
TextTrackRenderer.TextRenderer, StreamingDrmSessionManager.EventListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds renderers for the player.
|
* Builds renderers for the player.
|
||||||
|
@ -111,15 +111,19 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
|
|||||||
DrmSessionManager drmSessionManager = null;
|
DrmSessionManager drmSessionManager = null;
|
||||||
if (manifest.protectionElement != null) {
|
if (manifest.protectionElement != null) {
|
||||||
if (Util.SDK_INT < 18) {
|
if (Util.SDK_INT < 18) {
|
||||||
callback.onRenderersError(new UnsupportedOperationException(
|
callback.onRenderersError(
|
||||||
"Protected content not supported on API level " + Util.SDK_INT));
|
new UnsupportedDrmException(UnsupportedDrmException.REASON_NO_DRM));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
drmSessionManager = V18Compat.getDrmSessionManager(manifest.protectionElement.uuid, player,
|
drmSessionManager = V18Compat.getDrmSessionManager(manifest.protectionElement.uuid, player,
|
||||||
drmCallback);
|
drmCallback);
|
||||||
|
} catch (UnsupportedSchemeException e) {
|
||||||
|
callback.onRenderersError(
|
||||||
|
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
callback.onRenderersError(e);
|
callback.onRenderersError(
|
||||||
|
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNKNOWN, e));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,19 +153,28 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int[] videoTrackIndices = Util.toArray(videoTrackIndexList);
|
|
||||||
|
|
||||||
// Build the video renderer.
|
// Build the video renderer.
|
||||||
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
|
final MediaCodecVideoTrackRenderer videoRenderer;
|
||||||
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
final TrackRenderer debugRenderer;
|
||||||
videoStreamElementIndex, videoTrackIndices, videoDataSource,
|
if (videoTrackIndexList.isEmpty()) {
|
||||||
new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS);
|
videoRenderer = null;
|
||||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
debugRenderer = null;
|
||||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
|
} else {
|
||||||
DemoPlayer.TYPE_VIDEO);
|
int[] videoTrackIndices = Util.toArray(videoTrackIndexList);
|
||||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
|
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
|
||||||
drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null,
|
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||||
mainHandler, player, 50);
|
videoStreamElementIndex, videoTrackIndices, 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 renderer.
|
// Build the audio renderer.
|
||||||
final String[] audioTrackNames;
|
final String[] audioTrackNames;
|
||||||
@ -220,15 +233,10 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
|
|||||||
ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
||||||
TEXT_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, player, mainHandler.getLooper(),
|
||||||
mainHandler.getLooper());
|
new TtmlParser());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the debug renderer.
|
|
||||||
TrackRenderer debugRenderer = debugTextView != null
|
|
||||||
? new DebugTrackRenderer(debugTextView, videoRenderer, videoSampleSource)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// 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;
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer.demo.full.player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when the required level of DRM is not supported.
|
||||||
|
*/
|
||||||
|
public final class UnsupportedDrmException extends Exception {
|
||||||
|
|
||||||
|
public static final int REASON_NO_DRM = 0;
|
||||||
|
public static final int REASON_UNSUPPORTED_SCHEME = 1;
|
||||||
|
public static final int REASON_UNKNOWN = 2;
|
||||||
|
|
||||||
|
public final int reason;
|
||||||
|
|
||||||
|
public UnsupportedDrmException(int reason) {
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsupportedDrmException(int reason, Exception cause) {
|
||||||
|
super(cause);
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -39,7 +39,11 @@
|
|||||||
|
|
||||||
<string name="on">[on]</string>
|
<string name="on">[on]</string>
|
||||||
|
|
||||||
<string name="drm_not_supported">Protected content not supported on API levels below 18</string>
|
<string name="drm_error_not_supported">Protected content not supported on API levels below 18</string>
|
||||||
|
|
||||||
|
<string name="drm_error_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||||
|
|
||||||
|
<string name="drm_error_unknown">An unknown DRM error occurred</string>
|
||||||
|
|
||||||
<string name="failed">Playback failed</string>
|
<string name="failed">Playback failed</string>
|
||||||
|
|
||||||
|
@ -0,0 +1,316 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
|
||||||
|
import com.google.android.exoplayer.audio.AudioTrack;
|
||||||
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.media.AudioFormat;
|
||||||
|
import android.os.Handler;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders encoded AC-3/enhanced AC-3 data to an {@link AudioTrack} for decoding on the playback
|
||||||
|
* device.
|
||||||
|
*
|
||||||
|
* <p>To determine whether the playback device supports passthrough, receive an audio configuration
|
||||||
|
* using {@link AudioCapabilitiesReceiver} and check whether the audio capabilities include
|
||||||
|
* AC-3/enhanced AC-3 passthrough.
|
||||||
|
*/
|
||||||
|
@TargetApi(21)
|
||||||
|
public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface definition for a callback to be notified of {@link Ac3PassthroughAudioTrackRenderer}
|
||||||
|
* events.
|
||||||
|
*/
|
||||||
|
public interface EventListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when an {@link AudioTrack} fails to initialize.
|
||||||
|
*
|
||||||
|
* @param e The corresponding exception.
|
||||||
|
*/
|
||||||
|
void onAudioTrackInitializationError(AudioTrack.InitializationException e);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of a message that can be passed to an instance of this class via
|
||||||
|
* {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object
|
||||||
|
* should be a {@link Float} with 0 being silence and 1 being unity gain.
|
||||||
|
*/
|
||||||
|
public static final int MSG_SET_VOLUME = 1;
|
||||||
|
|
||||||
|
private static final int SOURCE_STATE_NOT_READY = 0;
|
||||||
|
private static final int SOURCE_STATE_READY = 1;
|
||||||
|
|
||||||
|
/** Default buffer size for AC-3 packets from the sample source */
|
||||||
|
private static final int DEFAULT_BUFFER_SIZE = 16384 * 2;
|
||||||
|
|
||||||
|
/** Multiplication factor for the audio track's buffer size. */
|
||||||
|
private static final int MIN_BUFFER_MULTIPLICATION_FACTOR = 3;
|
||||||
|
|
||||||
|
private final Handler eventHandler;
|
||||||
|
private final EventListener eventListener;
|
||||||
|
|
||||||
|
private final SampleSource source;
|
||||||
|
private final SampleHolder sampleHolder;
|
||||||
|
private final MediaFormatHolder formatHolder;
|
||||||
|
|
||||||
|
private int trackIndex;
|
||||||
|
private MediaFormat format;
|
||||||
|
|
||||||
|
private int sourceState;
|
||||||
|
private boolean inputStreamEnded;
|
||||||
|
private boolean shouldReadInputBuffer;
|
||||||
|
|
||||||
|
private long currentPositionUs;
|
||||||
|
|
||||||
|
private AudioTrack audioTrack;
|
||||||
|
private int audioSessionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new track renderer that passes AC-3 samples directly to an audio track.
|
||||||
|
*
|
||||||
|
* @param source The upstream source from which the renderer obtains samples.
|
||||||
|
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||||
|
* null if delivery of events is not required.
|
||||||
|
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||||
|
*/
|
||||||
|
public Ac3PassthroughAudioTrackRenderer(
|
||||||
|
SampleSource source, Handler eventHandler, EventListener eventListener) {
|
||||||
|
this.source = Assertions.checkNotNull(source);
|
||||||
|
this.eventHandler = eventHandler;
|
||||||
|
this.eventListener = eventListener;
|
||||||
|
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||||
|
sampleHolder.data = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE);
|
||||||
|
formatHolder = new MediaFormatHolder();
|
||||||
|
audioTrack = new AudioTrack(MIN_BUFFER_MULTIPLICATION_FACTOR);
|
||||||
|
shouldReadInputBuffer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isTimeSource() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int doPrepare() throws ExoPlaybackException {
|
||||||
|
try {
|
||||||
|
boolean sourcePrepared = source.prepare();
|
||||||
|
if (!sourcePrepared) {
|
||||||
|
return TrackRenderer.STATE_UNPREPARED;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ExoPlaybackException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < source.getTrackCount(); i++) {
|
||||||
|
// TODO(andrewlewis): Choose best format here after checking playout formats from HDMI config.
|
||||||
|
if (handlesMimeType(source.getTrackInfo(i).mimeType)) {
|
||||||
|
trackIndex = i;
|
||||||
|
return TrackRenderer.STATE_PREPARED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrackRenderer.STATE_IGNORE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean handlesMimeType(String mimeType) {
|
||||||
|
return MimeTypes.AUDIO_AC3.equals(mimeType) || MimeTypes.AUDIO_EC3.equals(mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onEnabled(long positionUs, boolean joining) {
|
||||||
|
source.enable(trackIndex, positionUs);
|
||||||
|
sourceState = SOURCE_STATE_NOT_READY;
|
||||||
|
inputStreamEnded = false;
|
||||||
|
currentPositionUs = positionUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||||
|
try {
|
||||||
|
sourceState = source.continueBuffering(positionUs)
|
||||||
|
? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState)
|
||||||
|
: SOURCE_STATE_NOT_READY;
|
||||||
|
|
||||||
|
if (format == null) {
|
||||||
|
readFormat();
|
||||||
|
} else {
|
||||||
|
// Initialize and start the audio track now.
|
||||||
|
if (!audioTrack.isInitialized()) {
|
||||||
|
int oldAudioSessionId = audioSessionId;
|
||||||
|
try {
|
||||||
|
audioSessionId = audioTrack.initialize(oldAudioSessionId);
|
||||||
|
} catch (AudioTrack.InitializationException e) {
|
||||||
|
notifyAudioTrackInitializationError(e);
|
||||||
|
throw new ExoPlaybackException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getState() == TrackRenderer.STATE_STARTED) {
|
||||||
|
audioTrack.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
feedInputBuffer();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ExoPlaybackException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readFormat() throws IOException {
|
||||||
|
int result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
||||||
|
if (result == SampleSource.FORMAT_READ) {
|
||||||
|
format = formatHolder.format;
|
||||||
|
// TODO: For E-AC-3 input, reconfigure with AudioFormat.ENCODING_E_AC3.
|
||||||
|
audioTrack.reconfigure(format.getFrameworkMediaFormatV16(), AudioFormat.ENCODING_AC3, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void feedInputBuffer() throws IOException {
|
||||||
|
if (!audioTrack.isInitialized() || inputStreamEnded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get more data if we have run out.
|
||||||
|
if (shouldReadInputBuffer) {
|
||||||
|
sampleHolder.data.clear();
|
||||||
|
|
||||||
|
int result =
|
||||||
|
source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
||||||
|
sampleHolder.data.flip();
|
||||||
|
shouldReadInputBuffer = false;
|
||||||
|
|
||||||
|
if (result == SampleSource.FORMAT_READ) {
|
||||||
|
format = formatHolder.format;
|
||||||
|
}
|
||||||
|
if (result == SampleSource.END_OF_STREAM) {
|
||||||
|
inputStreamEnded = true;
|
||||||
|
}
|
||||||
|
if (result != SampleSource.SAMPLE_READ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int handleBufferResult =
|
||||||
|
audioTrack.handleBuffer(sampleHolder.data, 0, sampleHolder.size, sampleHolder.timeUs);
|
||||||
|
|
||||||
|
// If we are out of sync, allow currentPositionUs to jump backwards.
|
||||||
|
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
|
||||||
|
currentPositionUs = Long.MIN_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get another input buffer if this one was consumed.
|
||||||
|
shouldReadInputBuffer = (handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStarted() {
|
||||||
|
if (audioTrack.isInitialized()) {
|
||||||
|
audioTrack.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStopped() {
|
||||||
|
if (audioTrack.isInitialized()) {
|
||||||
|
audioTrack.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isEnded() {
|
||||||
|
// We've exhausted the input stream, and the AudioTrack has either played all of the data
|
||||||
|
// submitted, or has been fed insufficient data to begin playback.
|
||||||
|
return inputStreamEnded && (!audioTrack.hasPendingData()
|
||||||
|
|| !audioTrack.hasEnoughDataToBeginPlayback());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isReady() {
|
||||||
|
return audioTrack.hasPendingData() || (format != null && sourceState != SOURCE_STATE_NOT_READY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long getCurrentPositionUs() {
|
||||||
|
long audioTrackCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded());
|
||||||
|
if (audioTrackCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) {
|
||||||
|
// Make sure we don't ever report time moving backwards.
|
||||||
|
currentPositionUs = Math.max(currentPositionUs, audioTrackCurrentPositionUs);
|
||||||
|
}
|
||||||
|
return currentPositionUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long getDurationUs() {
|
||||||
|
return source.getTrackInfo(trackIndex).durationUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long getBufferedPositionUs() {
|
||||||
|
long sourceBufferedPosition = source.getBufferedPositionUs();
|
||||||
|
return sourceBufferedPosition == UNKNOWN_TIME_US || sourceBufferedPosition == END_OF_TRACK_US
|
||||||
|
? sourceBufferedPosition : Math.max(sourceBufferedPosition, getCurrentPositionUs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDisabled() {
|
||||||
|
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||||
|
shouldReadInputBuffer = true;
|
||||||
|
audioTrack.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void seekTo(long positionUs) throws ExoPlaybackException {
|
||||||
|
source.seekToUs(positionUs);
|
||||||
|
sourceState = SOURCE_STATE_NOT_READY;
|
||||||
|
inputStreamEnded = false;
|
||||||
|
shouldReadInputBuffer = true;
|
||||||
|
|
||||||
|
// TODO: Try and re-use the same AudioTrack instance once [Internal: b/7941810] is fixed.
|
||||||
|
audioTrack.reset();
|
||||||
|
currentPositionUs = Long.MIN_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
|
||||||
|
if (messageType == MSG_SET_VOLUME) {
|
||||||
|
audioTrack.setVolume((Float) message);
|
||||||
|
} else {
|
||||||
|
super.handleMessage(messageType, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) {
|
||||||
|
if (eventHandler != null && eventListener != null) {
|
||||||
|
eventHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
eventListener.onAudioTrackInitializationError(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -88,13 +88,20 @@ public class MediaFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static MediaFormat createId3Format() {
|
public static MediaFormat createId3Format() {
|
||||||
return new MediaFormat(MimeTypes.APPLICATION_ID3, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
return createFormatForMimeType(MimeTypes.APPLICATION_ID3);
|
||||||
NO_VALUE, NO_VALUE, NO_VALUE, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MediaFormat createEia608Format() {
|
public static MediaFormat createEia608Format() {
|
||||||
return new MediaFormat(MimeTypes.APPLICATION_EIA608, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
return createFormatForMimeType(MimeTypes.APPLICATION_EIA608);
|
||||||
NO_VALUE, NO_VALUE, NO_VALUE, null);
|
}
|
||||||
|
|
||||||
|
public static MediaFormat createTtmlFormat() {
|
||||||
|
return createFormatForMimeType(MimeTypes.APPLICATION_TTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaFormat createFormatForMimeType(String mimeType) {
|
||||||
|
return new MediaFormat(mimeType, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||||
|
NO_VALUE, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(16)
|
@TargetApi(16)
|
||||||
|
@ -18,8 +18,6 @@ package com.google.android.exoplayer;
|
|||||||
import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
|
import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
import android.os.SystemClock;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a single component of media.
|
* Renders a single component of media.
|
||||||
*
|
*
|
||||||
@ -293,8 +291,8 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
|||||||
*
|
*
|
||||||
* @param positionUs The current media time in microseconds, measured at the start of the
|
* @param positionUs The current media time in microseconds, measured at the start of the
|
||||||
* current iteration of the rendering loop.
|
* current iteration of the rendering loop.
|
||||||
* @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at
|
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
|
||||||
* the start of the current iteration of the rendering loop.
|
* measured at the start of the current iteration of the rendering loop.
|
||||||
* @throws ExoPlaybackException If an error occurs.
|
* @throws ExoPlaybackException If an error occurs.
|
||||||
*/
|
*/
|
||||||
protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs)
|
protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs)
|
||||||
|
@ -272,16 +272,23 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
|||||||
downstreamPositionUs = positionUs;
|
downstreamPositionUs = positionUs;
|
||||||
chunkSource.continueBuffering(positionUs);
|
chunkSource.continueBuffering(positionUs);
|
||||||
updateLoadControl();
|
updateLoadControl();
|
||||||
|
|
||||||
|
boolean haveSamples = false;
|
||||||
if (isPendingReset() || mediaChunks.isEmpty()) {
|
if (isPendingReset() || mediaChunks.isEmpty()) {
|
||||||
return false;
|
// No sample available.
|
||||||
} else if (mediaChunks.getFirst().sampleAvailable()) {
|
} else if (mediaChunks.getFirst().sampleAvailable()) {
|
||||||
// There's a sample available to be read from the current chunk.
|
// There's a sample available to be read from the current chunk.
|
||||||
return true;
|
haveSamples = true;
|
||||||
} else {
|
} else {
|
||||||
// It may be the case that the current chunk has been fully read but not yet discarded and
|
// It may be the case that the current chunk has been fully read but not yet discarded and
|
||||||
// that the next chunk has an available sample. Return true if so, otherwise false.
|
// that the next chunk has an available sample. Return true if so, otherwise false.
|
||||||
return mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable();
|
haveSamples = mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!haveSamples) {
|
||||||
|
maybeThrowLoadableException();
|
||||||
|
}
|
||||||
|
return haveSamples;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -380,7 +387,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void maybeThrowLoadableException() throws IOException {
|
private void maybeThrowLoadableException() throws IOException {
|
||||||
if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) {
|
if (currentLoadableException != null && (currentLoadableExceptionFatal
|
||||||
|
|| currentLoadableExceptionCount > minLoadableRetryCount)) {
|
||||||
throw currentLoadableException;
|
throw currentLoadableException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -326,10 +326,13 @@ public class DashChunkSource implements ChunkSource {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int lastSegmentNum = segmentIndex.getLastSegmentNum();
|
||||||
|
boolean indexUnbounded = lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED;
|
||||||
|
|
||||||
int segmentNum;
|
int segmentNum;
|
||||||
if (queue.isEmpty()) {
|
if (queue.isEmpty()) {
|
||||||
if (currentManifest.dynamic) {
|
if (currentManifest.dynamic) {
|
||||||
seekPositionUs = getLiveSeekPosition();
|
seekPositionUs = getLiveSeekPosition(indexUnbounded);
|
||||||
}
|
}
|
||||||
segmentNum = segmentIndex.getSegmentNum(seekPositionUs);
|
segmentNum = segmentIndex.getSegmentNum(seekPositionUs);
|
||||||
} else {
|
} else {
|
||||||
@ -337,16 +340,18 @@ public class DashChunkSource implements ChunkSource {
|
|||||||
- representationHolder.segmentNumShift;
|
- representationHolder.segmentNumShift;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: For unbounded manifests, we need to enforce that we don't try and request chunks
|
||||||
|
// behind or in front of the live window.
|
||||||
if (currentManifest.dynamic) {
|
if (currentManifest.dynamic) {
|
||||||
if (segmentNum < segmentIndex.getFirstSegmentNum()) {
|
if (segmentNum < segmentIndex.getFirstSegmentNum()) {
|
||||||
// This is before the first chunk in the current manifest.
|
// This is before the first chunk in the current manifest.
|
||||||
fatalError = new BehindLiveWindowException();
|
fatalError = new BehindLiveWindowException();
|
||||||
return;
|
return;
|
||||||
} else if (segmentNum > segmentIndex.getLastSegmentNum()) {
|
} else if (!indexUnbounded && segmentNum > lastSegmentNum) {
|
||||||
// This is beyond the last chunk in the current manifest.
|
// This is beyond the last chunk in the current manifest.
|
||||||
finishedCurrentManifest = true;
|
finishedCurrentManifest = true;
|
||||||
return;
|
return;
|
||||||
} else if (segmentNum == segmentIndex.getLastSegmentNum()) {
|
} else if (!indexUnbounded && segmentNum == lastSegmentNum) {
|
||||||
// This is the last chunk in the current manifest. Mark the manifest as being finished,
|
// This is the last chunk in the current manifest. Mark the manifest as being finished,
|
||||||
// but continue to return the final chunk.
|
// but continue to return the final chunk.
|
||||||
finishedCurrentManifest = true;
|
finishedCurrentManifest = true;
|
||||||
@ -452,16 +457,24 @@ public class DashChunkSource implements ChunkSource {
|
|||||||
* For live playbacks, determines the seek position that snaps playback to be
|
* For live playbacks, determines the seek position that snaps playback to be
|
||||||
* {@link #liveEdgeLatencyUs} behind the live edge of the current manifest
|
* {@link #liveEdgeLatencyUs} behind the live edge of the current manifest
|
||||||
*
|
*
|
||||||
|
* @param indexUnbounded True if the segment index for this source is unbounded. False otherwise.
|
||||||
* @return The seek position in microseconds.
|
* @return The seek position in microseconds.
|
||||||
*/
|
*/
|
||||||
private long getLiveSeekPosition() {
|
private long getLiveSeekPosition(boolean indexUnbounded) {
|
||||||
long liveEdgeTimestampUs = Long.MIN_VALUE;
|
long liveEdgeTimestampUs;
|
||||||
for (RepresentationHolder representationHolder : representationHolders.values()) {
|
if (indexUnbounded) {
|
||||||
DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
|
// TODO: Use UtcTimingElement where possible.
|
||||||
int lastSegmentNum = segmentIndex.getLastSegmentNum();
|
long nowMs = System.currentTimeMillis();
|
||||||
long indexLiveEdgeTimestampUs = segmentIndex.getTimeUs(lastSegmentNum)
|
liveEdgeTimestampUs = (nowMs - currentManifest.availabilityStartTime) * 1000;
|
||||||
+ segmentIndex.getDurationUs(lastSegmentNum);
|
} else {
|
||||||
liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, indexLiveEdgeTimestampUs);
|
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;
|
return liveEdgeTimestampUs - liveEdgeLatencyUs;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,8 @@ import com.google.android.exoplayer.dash.mpd.RangedUri;
|
|||||||
*/
|
*/
|
||||||
public interface DashSegmentIndex {
|
public interface DashSegmentIndex {
|
||||||
|
|
||||||
|
public static final int INDEX_UNBOUNDED = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the segment number of the segment containing a given media time.
|
* Returns the segment number of the segment containing a given media time.
|
||||||
*
|
*
|
||||||
@ -64,9 +66,15 @@ public interface DashSegmentIndex {
|
|||||||
int getFirstSegmentNum();
|
int getFirstSegmentNum();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the segment number of the last segment.
|
* Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}.
|
||||||
|
* <p>
|
||||||
|
* An unbounded index occurs if a live stream manifest uses SegmentTemplate elements without a
|
||||||
|
* SegmentTimeline element. In this case the manifest can be used to derive information about
|
||||||
|
* segments arbitrarily far into the future. This means that the manifest does not need to be
|
||||||
|
* refreshed as frequently (if at all) during playback, however it is necessary for a player to
|
||||||
|
* manually calculate the window of currently available segments.
|
||||||
*
|
*
|
||||||
* @return The segment number of the last segment.
|
* @return The segment number of the last segment, or {@link #INDEX_UNBOUNDED}.
|
||||||
*/
|
*/
|
||||||
int getLastSegmentNum();
|
int getLastSegmentNum();
|
||||||
|
|
||||||
|
@ -356,7 +356,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected SegmentList parseSegmentList(XmlPullParser xpp, Uri baseUrl, SegmentList parent,
|
protected SegmentList parseSegmentList(XmlPullParser xpp, Uri baseUrl, SegmentList parent,
|
||||||
long periodDuration) throws XmlPullParserException, IOException {
|
long periodDurationMs) throws XmlPullParserException, IOException {
|
||||||
|
|
||||||
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
|
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
|
||||||
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
|
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
|
||||||
@ -388,19 +388,19 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
|||||||
segments = segments != null ? segments : parent.mediaSegments;
|
segments = segments != null ? segments : parent.mediaSegments;
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildSegmentList(initialization, timescale, presentationTimeOffset, periodDuration,
|
return buildSegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs,
|
||||||
startNumber, duration, timeline, segments);
|
startNumber, duration, timeline, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SegmentList buildSegmentList(RangedUri initialization, long timescale,
|
protected SegmentList buildSegmentList(RangedUri initialization, long timescale,
|
||||||
long presentationTimeOffset, long periodDuration, int startNumber, long duration,
|
long presentationTimeOffset, long periodDurationMs, int startNumber, long duration,
|
||||||
List<SegmentTimelineElement> timeline, List<RangedUri> segments) {
|
List<SegmentTimelineElement> timeline, List<RangedUri> segments) {
|
||||||
return new SegmentList(initialization, timescale, presentationTimeOffset, periodDuration,
|
return new SegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs,
|
||||||
startNumber, duration, timeline, segments);
|
startNumber, duration, timeline, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, Uri baseUrl,
|
protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, Uri baseUrl,
|
||||||
SegmentTemplate parent, long periodDuration) throws XmlPullParserException, IOException {
|
SegmentTemplate parent, long periodDurationMs) throws XmlPullParserException, IOException {
|
||||||
|
|
||||||
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
|
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
|
||||||
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
|
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
|
||||||
@ -429,15 +429,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
|||||||
timeline = timeline != null ? timeline : parent.segmentTimeline;
|
timeline = timeline != null ? timeline : parent.segmentTimeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, periodDuration,
|
return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs,
|
||||||
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
|
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale,
|
protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale,
|
||||||
long presentationTimeOffset, long periodDuration, int startNumber, long duration,
|
long presentationTimeOffset, long periodDurationMs, int startNumber, long duration,
|
||||||
List<SegmentTimelineElement> timeline, UrlTemplate initializationTemplate,
|
List<SegmentTimelineElement> timeline, UrlTemplate initializationTemplate,
|
||||||
UrlTemplate mediaTemplate, Uri baseUrl) {
|
UrlTemplate mediaTemplate, Uri baseUrl) {
|
||||||
return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDuration,
|
return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs,
|
||||||
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
|
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,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.C;
|
||||||
|
import com.google.android.exoplayer.dash.DashSegmentIndex;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -127,17 +128,28 @@ public abstract class SegmentBase {
|
|||||||
this.segmentTimeline = segmentTimeline;
|
this.segmentTimeline = segmentTimeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final int getSegmentNum(long timeUs) {
|
public int getSegmentNum(long timeUs) {
|
||||||
// TODO: Optimize this
|
if (segmentTimeline == null) {
|
||||||
int index = startNumber;
|
// All segments are of equal duration (with the possible exception of the last one).
|
||||||
while (index + 1 <= getLastSegmentNum()) {
|
long durationUs = (duration * C.MICROS_PER_SECOND) / timescale;
|
||||||
if (getSegmentTimeUs(index + 1) <= timeUs) {
|
return startNumber + (int) (timeUs / durationUs);
|
||||||
index++;
|
} else {
|
||||||
} else {
|
// Identify the segment using binary search.
|
||||||
return index;
|
int lowIndex = getFirstSegmentNum();
|
||||||
|
int highIndex = getLastSegmentNum();
|
||||||
|
while (lowIndex <= highIndex) {
|
||||||
|
int midIndex = (lowIndex + highIndex) / 2;
|
||||||
|
long midTimeUs = getSegmentTimeUs(midIndex);
|
||||||
|
if (midTimeUs < timeUs) {
|
||||||
|
lowIndex = midIndex + 1;
|
||||||
|
} else if (midTimeUs > timeUs) {
|
||||||
|
highIndex = midIndex - 1;
|
||||||
|
} else {
|
||||||
|
return midIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return lowIndex - 1;
|
||||||
}
|
}
|
||||||
return index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final long getSegmentDurationUs(int sequenceNumber) {
|
public final long getSegmentDurationUs(int sequenceNumber) {
|
||||||
@ -285,6 +297,8 @@ public abstract class SegmentBase {
|
|||||||
public int getLastSegmentNum() {
|
public int getLastSegmentNum() {
|
||||||
if (segmentTimeline != null) {
|
if (segmentTimeline != null) {
|
||||||
return segmentTimeline.size() + startNumber - 1;
|
return segmentTimeline.size() + startNumber - 1;
|
||||||
|
} else if (periodDurationMs == -1) {
|
||||||
|
return DashSegmentIndex.INDEX_UNBOUNDED;
|
||||||
} else {
|
} else {
|
||||||
long durationMs = (duration * 1000) / timescale;
|
long durationMs = (duration * 1000) / timescale;
|
||||||
return startNumber + (int) (periodDurationMs / durationMs);
|
return startNumber + (int) (periodDurationMs / durationMs);
|
||||||
|
@ -58,6 +58,7 @@ import java.util.ArrayList;
|
|||||||
public static final int TYPE_uuid = 0x75756964;
|
public static final int TYPE_uuid = 0x75756964;
|
||||||
public static final int TYPE_senc = 0x73656E63;
|
public static final int TYPE_senc = 0x73656E63;
|
||||||
public static final int TYPE_pasp = 0x70617370;
|
public static final int TYPE_pasp = 0x70617370;
|
||||||
|
public static final int TYPE_TTML = 0x54544D4C;
|
||||||
|
|
||||||
public final int type;
|
public final int type;
|
||||||
|
|
||||||
|
@ -428,7 +428,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||||||
private static Track parseTrak(ContainerAtom trak) {
|
private static Track parseTrak(ContainerAtom trak) {
|
||||||
ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
|
ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
|
||||||
int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
|
int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
|
||||||
Assertions.checkState(trackType == Track.TYPE_AUDIO || trackType == Track.TYPE_VIDEO);
|
Assertions.checkState(trackType == Track.TYPE_AUDIO || trackType == Track.TYPE_VIDEO
|
||||||
|
|| trackType == Track.TYPE_TEXT);
|
||||||
|
|
||||||
Pair<Integer, Long> header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
|
Pair<Integer, Long> header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
|
||||||
int id = header.first;
|
int id = header.first;
|
||||||
@ -528,6 +529,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||||||
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize);
|
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize);
|
||||||
mediaFormat = audioSampleEntry.first;
|
mediaFormat = audioSampleEntry.first;
|
||||||
trackEncryptionBoxes[i] = audioSampleEntry.second;
|
trackEncryptionBoxes[i] = audioSampleEntry.second;
|
||||||
|
} else if (childAtomType == Atom.TYPE_TTML) {
|
||||||
|
mediaFormat = MediaFormat.createTtmlFormat();
|
||||||
}
|
}
|
||||||
stsd.setPosition(childStartPosition + childAtomSize);
|
stsd.setPosition(childStartPosition + childAtomSize);
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,10 @@ public final class Track {
|
|||||||
* Type of an audio track.
|
* Type of an audio track.
|
||||||
*/
|
*/
|
||||||
public static final int TYPE_AUDIO = 0x736F756E;
|
public static final int TYPE_AUDIO = 0x736F756E;
|
||||||
|
/**
|
||||||
|
* Type of a text track.
|
||||||
|
*/
|
||||||
|
public static final int TYPE_TEXT = 0x74657874;
|
||||||
/**
|
/**
|
||||||
* Type of a hint track.
|
* Type of a hint track.
|
||||||
*/
|
*/
|
||||||
|
@ -358,8 +358,9 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
MediaFormat format = MediaFormat.createAudioFormat(mimeType, -1, trackElement.numChannels,
|
MediaFormat format = MediaFormat.createAudioFormat(mimeType, -1, trackElement.numChannels,
|
||||||
trackElement.sampleRate, csd);
|
trackElement.sampleRate, csd);
|
||||||
return format;
|
return format;
|
||||||
|
} else if (streamElement.type == StreamElement.TYPE_TEXT) {
|
||||||
|
return MediaFormat.createFormatForMimeType(streamElement.tracks[trackIndex].mimeType);
|
||||||
}
|
}
|
||||||
// TODO: Do subtitles need a format? MediaFormat supports KEY_LANGUAGE.
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,8 +58,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
private final TextRenderer textRenderer;
|
private final TextRenderer textRenderer;
|
||||||
private final SampleSource source;
|
private final SampleSource source;
|
||||||
private final MediaFormatHolder formatHolder;
|
private final MediaFormatHolder formatHolder;
|
||||||
private final SubtitleParser subtitleParser;
|
private final SubtitleParser[] subtitleParsers;
|
||||||
|
|
||||||
|
private int parserIndex;
|
||||||
private int trackIndex;
|
private int trackIndex;
|
||||||
|
|
||||||
private long currentPositionUs;
|
private long currentPositionUs;
|
||||||
@ -73,21 +74,22 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param source A source from which samples containing subtitle data can be read.
|
* @param source A source from which samples containing subtitle data can be read.
|
||||||
* @param subtitleParser A subtitle parser that will parse Subtitle objects from the source.
|
|
||||||
* @param textRenderer The text renderer.
|
* @param textRenderer The text renderer.
|
||||||
* @param textRendererLooper The looper associated with the thread on which textRenderer should be
|
* @param textRendererLooper The looper associated with the thread on which textRenderer should be
|
||||||
* invoked. If the renderer makes use of standard Android UI components, then this should
|
* invoked. If the renderer makes use of standard Android UI components, then this should
|
||||||
* normally be the looper associated with the applications' main thread, which can be
|
* normally be the looper associated with the applications' main thread, which can be
|
||||||
* obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the
|
* obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the
|
||||||
* renderer should be invoked directly on the player's internal rendering thread.
|
* renderer should be invoked directly on the player's internal rendering thread.
|
||||||
|
* @param subtitleParsers An array of available subtitle parsers. Where multiple parsers are able
|
||||||
|
* to render a subtitle, the one with the lowest index will be preferred.
|
||||||
*/
|
*/
|
||||||
public TextTrackRenderer(SampleSource source, SubtitleParser subtitleParser,
|
public TextTrackRenderer(SampleSource source, TextRenderer textRenderer,
|
||||||
TextRenderer textRenderer, Looper textRendererLooper) {
|
Looper textRendererLooper, SubtitleParser... subtitleParsers) {
|
||||||
this.source = Assertions.checkNotNull(source);
|
this.source = Assertions.checkNotNull(source);
|
||||||
this.subtitleParser = Assertions.checkNotNull(subtitleParser);
|
|
||||||
this.textRenderer = Assertions.checkNotNull(textRenderer);
|
this.textRenderer = Assertions.checkNotNull(textRenderer);
|
||||||
this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper,
|
this.textRendererHandler = textRendererLooper == null ? null
|
||||||
this);
|
: new Handler(textRendererLooper, this);
|
||||||
|
this.subtitleParsers = Assertions.checkNotNull(subtitleParsers);
|
||||||
formatHolder = new MediaFormatHolder();
|
formatHolder = new MediaFormatHolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,10 +103,13 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ExoPlaybackException(e);
|
throw new ExoPlaybackException(e);
|
||||||
}
|
}
|
||||||
for (int i = 0; i < source.getTrackCount(); i++) {
|
for (int i = 0; i < subtitleParsers.length; i++) {
|
||||||
if (subtitleParser.canParse(source.getTrackInfo(i).mimeType)) {
|
for (int j = 0; j < source.getTrackCount(); j++) {
|
||||||
trackIndex = i;
|
if (subtitleParsers[i].canParse(source.getTrackInfo(j).mimeType)) {
|
||||||
return TrackRenderer.STATE_PREPARED;
|
parserIndex = i;
|
||||||
|
trackIndex = j;
|
||||||
|
return TrackRenderer.STATE_PREPARED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return TrackRenderer.STATE_IGNORE;
|
return TrackRenderer.STATE_IGNORE;
|
||||||
@ -115,7 +120,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
source.enable(trackIndex, positionUs);
|
source.enable(trackIndex, positionUs);
|
||||||
parserThread = new HandlerThread("textParser");
|
parserThread = new HandlerThread("textParser");
|
||||||
parserThread.start();
|
parserThread.start();
|
||||||
parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParser);
|
parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParsers[parserIndex]);
|
||||||
seekToInternal(positionUs);
|
seekToInternal(positionUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,6 +194,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
|
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
|
||||||
if (result == SampleSource.SAMPLE_READ) {
|
if (result == SampleSource.SAMPLE_READ) {
|
||||||
parserHelper.startParseOperation();
|
parserHelper.startParseOperation();
|
||||||
|
textRendererNeedsUpdate = false;
|
||||||
} else if (result == SampleSource.END_OF_STREAM) {
|
} else if (result == SampleSource.END_OF_STREAM) {
|
||||||
inputStreamEnded = true;
|
inputStreamEnded = true;
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,6 @@ import java.util.regex.Pattern;
|
|||||||
* A simple WebVTT parser.
|
* A simple WebVTT parser.
|
||||||
* <p>
|
* <p>
|
||||||
* @see <a href="http://dev.w3.org/html5/webvtt">WebVTT specification</a>
|
* @see <a href="http://dev.w3.org/html5/webvtt">WebVTT specification</a>
|
||||||
* <p>
|
|
||||||
*/
|
*/
|
||||||
public class WebvttParser implements SubtitleParser {
|
public class WebvttParser implements SubtitleParser {
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user