Merge branch 'dev' into dev-hls

This commit is contained in:
Oliver Woodman 2014-12-12 14:24:05 +00:00
commit 4c29eb58f1
22 changed files with 657 additions and 157 deletions

View File

@ -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)

View File

@ -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() {}

View File

@ -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();

View File

@ -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.

View File

@ -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.

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);
}
});
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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;
} }
} }

View File

@ -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;
} }

View File

@ -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();

View File

@ -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);
} }

View File

@ -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);

View File

@ -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;

View File

@ -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);
} }

View File

@ -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.
*/ */

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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 {