diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java index 14827f05c2..94478c48e7 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer.demo; import com.google.android.exoplayer.demo.Samples.Sample; import com.google.android.exoplayer.demo.full.FullPlayerActivity; import com.google.android.exoplayer.demo.simple.SimplePlayerActivity; -import com.google.android.exoplayer.util.Util; import android.app.Activity; import android.content.Context; @@ -33,7 +32,6 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; -import android.widget.Toast; /** * An activity for selecting from a number of samples. @@ -78,11 +76,6 @@ public class SampleChooserActivity extends Activity { } 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 : SimplePlayerActivity.class; Intent mpdIntent = new Intent(this, playerActivityClass) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index b71c29b106..b3eb4af93c 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -26,16 +26,13 @@ package com.google.android.exoplayer.demo; public final String contentId; public final String uri; public final int type; - public final boolean isEncypted; public final boolean fullPlayer; - public Sample(String name, String contentId, String uri, int type, boolean isEncrypted, - boolean fullPlayer) { + public Sample(String name, String contentId, String uri, int type, boolean fullPlayer) { this.name = name; this.contentId = contentId; this.uri = uri; this.type = type; - this.isEncypted = isEncrypted; this.fullPlayer = fullPlayer; } @@ -46,22 +43,20 @@ package com.google.android.exoplayer.demo; "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&" + "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D." - + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false, - false), + + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false), new Sample("Google Play (DASH)", "3aa39fa2cc27967f", "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&" + "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A." - + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false, - false), + + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false), new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed", "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", "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", - "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[] { @@ -69,14 +64,12 @@ package com.google.android.exoplayer.demo; "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&" + "ipbits=0&expire=19000000000&signature=255F6B3C07C753C88708C07EA31B7A1A10703C8D." - + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, false, - true), + + "2D6A28B21F921D0B245CDCF36F7EB54A2B5ABFC2&key=ik0", DemoUtil.TYPE_DASH, true), new Sample("Google Play", "3aa39fa2cc27967f", "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&" + "expire=19000000000&signature=7181C59D0252B285D593E1B61D985D5B7C98DE2A." - + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, false, - true), + + "5B445837F55A40E0F28AACAA047982E372D177E2&key=ik0", DemoUtil.TYPE_DASH, true), }; 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?" + "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&" + "expire=19000000000&signature=A3EC7EE53ABE601B357F7CAB8B54AD0702CA85A7." - + "446E9C38E47E3EDAF39E0163C390FF83A7944918&key=ik0", DemoUtil.TYPE_DASH, false, true), + + "446E9C38E47E3EDAF39E0163C390FF83A7944918&key=ik0", DemoUtil.TYPE_DASH, true), new Sample("Google Play", "3aa39fa2cc27967f", "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" + "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0&" + "expire=19000000000&signature=B752B262C6D7262EC4E4EB67901E5D8F7058A81D." - + "C0358CE1E335417D9A8D88FF192F0D5D8F6DA1B6&key=ik0", DemoUtil.TYPE_DASH, false, true), + + "C0358CE1E335417D9A8D88FF192F0D5D8F6DA1B6&key=ik0", DemoUtil.TYPE_DASH, true), }; public static final Sample[] SMOOTHSTREAMING = new Sample[] { new Sample("Super speed", "uid:ss:superspeed", "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", "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[] { @@ -106,54 +99,54 @@ package com.google.android.exoplayer.demo; "http://www.youtube.com/api/manifest/dash/id/d286538032258a1c/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=41EA40A027A125A16292E0A5E3277A3B5FA9B938." - + "0BB075C396FFDDC97E526E8F77DC26FF9667D0D6&key=ik0", DemoUtil.TYPE_DASH, true, true), + + "0BB075C396FFDDC97E526E8F77DC26FF9667D0D6&key=ik0", DemoUtil.TYPE_DASH, true), new Sample("WV: HDCP not required", "48fcc369939ac96c", "http://www.youtube.com/api/manifest/dash/id/48fcc369939ac96c/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=315911BDCEED0FB0C763455BDCC97449DAAFA9E8." - + "5B41E2EB411F797097A359D6671D2CDE26272373&key=ik0", DemoUtil.TYPE_DASH, true, true), + + "5B41E2EB411F797097A359D6671D2CDE26272373&key=ik0", DemoUtil.TYPE_DASH, true), new Sample("WV: HDCP required", "e06c39f1151da3df", "http://www.youtube.com/api/manifest/dash/id/e06c39f1151da3df/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=A47A1E13E7243BD567601A75F79B34644D0DC592." - + "B09589A34FA23527EFC1552907754BB8033870BD&key=ik0", DemoUtil.TYPE_DASH, true, true), + + "B09589A34FA23527EFC1552907754BB8033870BD&key=ik0", DemoUtil.TYPE_DASH, true), new Sample("WV: Secure video path required", "0894c7c8719b28a0", "http://www.youtube.com/api/manifest/dash/id/0894c7c8719b28a0/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=2847EE498970F6B45176766CD2802FEB4D4CB7B2." - + "A1CA51EC40A1C1039BA800C41500DD448C03EEDA&key=ik0", DemoUtil.TYPE_DASH, true, true), + + "A1CA51EC40A1C1039BA800C41500DD448C03EEDA&key=ik0", DemoUtil.TYPE_DASH, true), new Sample("WV: HDCP + secure video path required", "efd045b1eb61888a", "http://www.youtube.com/api/manifest/dash/id/efd045b1eb61888a/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=61611F115EEEC7BADE5536827343FFFE2D83D14F." - + "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH, true, true), + + "2FDF4BFA502FB5865C5C86401314BDDEA4799BD0&key=ik0", DemoUtil.TYPE_DASH, true), new Sample("WV: 30s license duration", "f9a34cab7b05881a", "http://www.youtube.com/api/manifest/dash/id/f9a34cab7b05881a/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0" + "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6." - + "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH, true, true), + + "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH, true), }; public static final Sample[] HLS = new Sample[] { new Sample("Apple master playlist", "uid:hls:applemaster", "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", "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", "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[] { 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", - DemoUtil.TYPE_OTHER, false, true), + DemoUtil.TYPE_OTHER, true), new Sample("Apple AAC 10s", "uid:misc:appleaacseg", "https://devimages.apple.com.edgekey.net/" + "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac", - DemoUtil.TYPE_OTHER, false, true), + DemoUtil.TYPE_OTHER, true), }; private Samples() {} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 6b479db7d0..eb2ad94ebf 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer.demo.full; import com.google.android.exoplayer.ExoPlayer; 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.R; 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.HlsRendererBuilder; 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.text.CaptionStyleCompat; import com.google.android.exoplayer.text.SubtitleView; @@ -55,6 +58,7 @@ import android.widget.MediaController; import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.TextView; +import android.widget.Toast; import java.util.Map; @@ -62,7 +66,8 @@ import java.util.Map; * An activity that plays media using {@link DemoPlayer}. */ 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"; @@ -94,6 +99,9 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba private int contentType; private String contentId; + private AudioCapabilitiesReceiver audioCapabilitiesReceiver; + private AudioCapabilities audioCapabilities; + // Activity lifecycle @Override @@ -117,6 +125,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } }); + audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this); + shutterView = findViewById(R.id.shutter); debugRootView = findViewById(R.id.controls_root); @@ -142,7 +152,9 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba public void onResume() { super.onResume(); configureSubtitleView(); - preparePlayer(); + + // The player will be prepared on receiving audio capabilities. + audioCapabilitiesReceiver.register(); } @Override @@ -153,6 +165,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } else { player.blockingClearSurface(); } + + audioCapabilitiesReceiver.unregister(); } @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 private RendererBuilder getRendererBuilder() { @@ -181,7 +206,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba new SmoothStreamingTestMediaDrmCallback(), debugTextView); case DemoUtil.TYPE_DASH: return new DashRendererBuilder(userAgent, contentUri.toString(), contentId, - new WidevineTestMediaDrmCallback(contentId), debugTextView); + new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities); case DemoUtil.TYPE_HLS: return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId); default: @@ -266,6 +291,16 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba @Override 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; updateButtonVisibilities(); showControls(); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java index 8ffd60218a..bbc9e868e1 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java @@ -15,6 +15,7 @@ */ 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.LoadControl; 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.SampleSource; 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.ChunkSource; 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.StreamingDrmSessionManager; 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.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; @@ -83,18 +86,20 @@ public class DashRendererBuilder implements RendererBuilder, private final String contentId; private final MediaDrmCallback drmCallback; private final TextView debugTextView; + private final AudioCapabilities audioCapabilities; private DemoPlayer player; private RendererBuilderCallback callback; private ManifestFetcher manifestFetcher; public DashRendererBuilder(String userAgent, String url, String contentId, - MediaDrmCallback drmCallback, TextView debugTextView) { + MediaDrmCallback drmCallback, TextView debugTextView, AudioCapabilities audioCapabilities) { this.userAgent = userAgent; this.url = url; this.contentId = contentId; this.drmCallback = drmCallback; this.debugTextView = debugTextView; + this.audioCapabilities = audioCapabilities; } @Override @@ -119,17 +124,33 @@ public class DashRendererBuilder implements RendererBuilder, LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); + boolean hasContentProtection = false; 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. - boolean hasContentProtection = videoAdaptationSet.hasContentProtection(); boolean filterHdContent = false; DrmSessionManager drmSessionManager = null; if (hasContentProtection) { if (Util.SDK_INT < 18) { - callback.onRenderersError(new UnsupportedOperationException( - "Protected content not supported on API level " + Util.SDK_INT)); + callback.onRenderersError( + new UnsupportedDrmException(UnsupportedDrmException.REASON_NO_DRM)); return; } try { @@ -137,28 +158,35 @@ public class DashRendererBuilder implements RendererBuilder, V18Compat.getDrmSessionManagerData(player, drmCallback); drmSessionManager = drmSessionManagerData.first; // 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) { - callback.onRenderersError(e); + callback.onRenderersError( + new UnsupportedDrmException(UnsupportedDrmException.REASON_UNKNOWN, e)); return; } } // Determine which video representations we should use for playback. - int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); - List videoRepresentations = videoAdaptationSet.representations; ArrayList videoRepresentationIndexList = new ArrayList(); - for (int i = 0; i < videoRepresentations.size(); i++) { - Format format = videoRepresentations.get(i).format; - if (filterHdContent && (format.width >= 1280 || format.height >= 720)) { - // Filtering HD content - } else if (format.width * format.height > maxDecodableFrameSize) { - // Filtering stream that device cannot play - } else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4) - && !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) { - // Filtering unsupported mime type - } else { - videoRepresentationIndexList.add(i); + if (videoAdaptationSet != null) { + int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); + List videoRepresentations = videoAdaptationSet.representations; + for (int i = 0; i < videoRepresentations.size(); i++) { + Format format = videoRepresentations.get(i).format; + if (filterHdContent && (format.width >= 1280 || format.height >= 720)) { + // Filtering HD content + } else if (format.width * format.height > maxDecodableFrameSize) { + // Filtering stream that device cannot play + } else if (!format.mimeType.equals(MimeTypes.VIDEO_MP4) + && !format.mimeType.equals(MimeTypes.VIDEO_WEBM)) { + // Filtering unsupported mime type + } else { + videoRepresentationIndexList.add(i); + } } } @@ -184,19 +212,33 @@ public class DashRendererBuilder implements RendererBuilder, } // Build the audio chunk sources. - int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO); - AdaptationSet audioAdaptationSet = period.adaptationSets.get(audioAdaptationSetIndex); - DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); - FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); + boolean haveAc3Tracks = false; List audioChunkSourceList = new ArrayList(); List audioTrackNameList = new ArrayList(); - List audioRepresentations = audioAdaptationSet.representations; - for (int i = 0; i < audioRepresentations.size(); i++) { - Format format = audioRepresentations.get(i).format; - audioTrackNameList.add(format.id + " (" + format.numChannels + "ch, " + - format.audioSamplingRate + "Hz)"); - audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex, - new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS)); + if (audioAdaptationSet != null) { + DataSource audioDataSource = new UriDataSource(userAgent, bandwidthMeter); + FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); + List audioRepresentations = audioAdaptationSet.representations; + for (int i = 0; i < audioRepresentations.size(); i++) { + Format format = audioRepresentations.get(i).format; + audioTrackNameList.add(format.id + " (" + format.numChannels + "ch, " + + format.audioSamplingRate + "Hz)"); + audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex, + new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS)); + 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. @@ -214,8 +256,16 @@ public class DashRendererBuilder implements RendererBuilder, SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_AUDIO); - audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true, - mainHandler, player); + // TODO: There needs to be some logic to filter out non-AC3 tracks when selecting to use AC3. + 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. @@ -251,8 +301,8 @@ public class DashRendererBuilder implements RendererBuilder, SampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_TEXT); - textRenderer = new TextTrackRenderer(textSampleSource, new WebvttParser(), player, - mainHandler.getLooper()); + textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper(), + new TtmlParser(), new WebvttParser()); } // Invoke the callback. diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index 816312cee8..1a9b4e2909 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -15,6 +15,7 @@ */ 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.ExoPlaybackException; import com.google.android.exoplayer.ExoPlayer; @@ -49,8 +50,8 @@ import java.util.concurrent.CopyOnWriteArrayList; */ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, - MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer, - StreamingDrmSessionManager.EventListener { + MediaCodecAudioTrackRenderer.EventListener, Ac3PassthroughAudioTrackRenderer.EventListener, + TextTrackRenderer.TextRenderer, StreamingDrmSessionManager.EventListener { /** * Builds renderers for the player. diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index fd9c220cb2..e02ebe5ca8 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -111,15 +111,19 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, DrmSessionManager drmSessionManager = null; if (manifest.protectionElement != null) { if (Util.SDK_INT < 18) { - callback.onRenderersError(new UnsupportedOperationException( - "Protected content not supported on API level " + Util.SDK_INT)); + callback.onRenderersError( + new UnsupportedDrmException(UnsupportedDrmException.REASON_NO_DRM)); return; } try { drmSessionManager = V18Compat.getDrmSessionManager(manifest.protectionElement.uuid, player, drmCallback); + } catch (UnsupportedSchemeException e) { + callback.onRenderersError( + new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e)); } catch (Exception e) { - callback.onRenderersError(e); + callback.onRenderersError( + new UnsupportedDrmException(UnsupportedDrmException.REASON_UNKNOWN, e)); return; } } @@ -149,19 +153,28 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } } } - int[] videoTrackIndices = Util.toArray(videoTrackIndexList); // Build the video renderer. - DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); - ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher, - 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); - MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, - drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, - mainHandler, player, 50); + final MediaCodecVideoTrackRenderer videoRenderer; + final TrackRenderer debugRenderer; + if (videoTrackIndexList.isEmpty()) { + videoRenderer = null; + debugRenderer = null; + } else { + int[] videoTrackIndices = Util.toArray(videoTrackIndexList); + DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); + ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher, + 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. final String[] audioTrackNames; @@ -220,15 +233,10 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl, TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_TEXT); - textRenderer = new TextTrackRenderer(ttmlSampleSource, new TtmlParser(), player, - mainHandler.getLooper()); + textRenderer = new TextTrackRenderer(ttmlSampleSource, player, mainHandler.getLooper(), + new TtmlParser()); } - // Build the debug renderer. - TrackRenderer debugRenderer = debugTextView != null - ? new DebugTrackRenderer(debugTextView, videoRenderer, videoSampleSource) - : null; - // Invoke the callback. String[][] trackNames = new String[DemoPlayer.RENDERER_COUNT][]; trackNames[DemoPlayer.TYPE_AUDIO] = audioTrackNames; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/UnsupportedDrmException.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/UnsupportedDrmException.java new file mode 100644 index 0000000000..3776b8bef5 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/UnsupportedDrmException.java @@ -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; + } + +} diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml index f71c2fb377..3b2b190a1b 100644 --- a/demo/src/main/res/values/strings.xml +++ b/demo/src/main/res/values/strings.xml @@ -39,7 +39,11 @@ [on] - Protected content not supported on API levels below 18 + Protected content not supported on API levels below 18 + + This device does not support the required DRM scheme + + An unknown DRM error occurred Playback failed diff --git a/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java new file mode 100644 index 0000000000..bfbe3b56d3 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java @@ -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. + * + *

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); + } + }); + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index bc1f1670af..18f47bf8b0 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -88,13 +88,20 @@ public class MediaFormat { } public static MediaFormat createId3Format() { - return new MediaFormat(MimeTypes.APPLICATION_ID3, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null); + return createFormatForMimeType(MimeTypes.APPLICATION_ID3); } public static MediaFormat createEia608Format() { - return new MediaFormat(MimeTypes.APPLICATION_EIA608, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null); + return createFormatForMimeType(MimeTypes.APPLICATION_EIA608); + } + + 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) diff --git a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java index cf4f8f13fc..8d5534e0b7 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java @@ -18,8 +18,6 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer.util.Assertions; -import android.os.SystemClock; - /** * 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 * current iteration of the rendering loop. - * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at - * the start of the current iteration of the rendering loop. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. * @throws ExoPlaybackException If an error occurs. */ protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs) diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index f16fc4c4df..038289d841 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -272,16 +272,23 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { downstreamPositionUs = positionUs; chunkSource.continueBuffering(positionUs); updateLoadControl(); + + boolean haveSamples = false; if (isPendingReset() || mediaChunks.isEmpty()) { - return false; + // No sample available. } else if (mediaChunks.getFirst().sampleAvailable()) { // There's a sample available to be read from the current chunk. - return true; + haveSamples = true; } else { // 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. - return mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable(); + haveSamples = mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable(); } + + if (!haveSamples) { + maybeThrowLoadableException(); + } + return haveSamples; } @Override @@ -380,7 +387,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } private void maybeThrowLoadableException() throws IOException { - if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { + if (currentLoadableException != null && (currentLoadableExceptionFatal + || currentLoadableExceptionCount > minLoadableRetryCount)) { throw currentLoadableException; } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 932a8ea598..8f0dc8b71c 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -326,10 +326,13 @@ public class DashChunkSource implements ChunkSource { return; } + int lastSegmentNum = segmentIndex.getLastSegmentNum(); + boolean indexUnbounded = lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; + int segmentNum; if (queue.isEmpty()) { if (currentManifest.dynamic) { - seekPositionUs = getLiveSeekPosition(); + seekPositionUs = getLiveSeekPosition(indexUnbounded); } segmentNum = segmentIndex.getSegmentNum(seekPositionUs); } else { @@ -337,16 +340,18 @@ public class DashChunkSource implements ChunkSource { - 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 (segmentNum < segmentIndex.getFirstSegmentNum()) { // This is before the first chunk in the current manifest. fatalError = new BehindLiveWindowException(); return; - } else if (segmentNum > segmentIndex.getLastSegmentNum()) { + } else if (!indexUnbounded && segmentNum > lastSegmentNum) { // This is beyond the last chunk in the current manifest. finishedCurrentManifest = true; 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, // but continue to return the final chunk. finishedCurrentManifest = true; @@ -452,16 +457,24 @@ public class DashChunkSource implements ChunkSource { * For live playbacks, determines the seek position that snaps playback to be * {@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. */ - private long getLiveSeekPosition() { - long liveEdgeTimestampUs = Long.MIN_VALUE; - for (RepresentationHolder representationHolder : representationHolders.values()) { - DashSegmentIndex segmentIndex = representationHolder.segmentIndex; - int lastSegmentNum = segmentIndex.getLastSegmentNum(); - long indexLiveEdgeTimestampUs = segmentIndex.getTimeUs(lastSegmentNum) - + segmentIndex.getDurationUs(lastSegmentNum); - liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, indexLiveEdgeTimestampUs); + private long getLiveSeekPosition(boolean indexUnbounded) { + long liveEdgeTimestampUs; + if (indexUnbounded) { + // TODO: Use UtcTimingElement where possible. + long nowMs = System.currentTimeMillis(); + liveEdgeTimestampUs = (nowMs - currentManifest.availabilityStartTime) * 1000; + } else { + 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; } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java index 336e4c6057..e66aa38380 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java @@ -24,6 +24,8 @@ import com.google.android.exoplayer.dash.mpd.RangedUri; */ public interface DashSegmentIndex { + public static final int INDEX_UNBOUNDED = -1; + /** * Returns the segment number of the segment containing a given media time. * @@ -64,9 +66,15 @@ public interface DashSegmentIndex { int getFirstSegmentNum(); /** - * Returns the segment number of the last segment. + * Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}. + *

+ * 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(); diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index a8ed7c03f2..4aa624cca1 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -356,7 +356,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } 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 presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -388,19 +388,19 @@ public class MediaPresentationDescriptionParser extends DefaultHandler segments = segments != null ? segments : parent.mediaSegments; } - return buildSegmentList(initialization, timescale, presentationTimeOffset, periodDuration, + return buildSegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, duration, timeline, segments); } protected SegmentList buildSegmentList(RangedUri initialization, long timescale, - long presentationTimeOffset, long periodDuration, int startNumber, long duration, + long presentationTimeOffset, long periodDurationMs, int startNumber, long duration, List timeline, List segments) { - return new SegmentList(initialization, timescale, presentationTimeOffset, periodDuration, + return new SegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, duration, timeline, segments); } 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 presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -429,15 +429,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler timeline = timeline != null ? timeline : parent.segmentTimeline; } - return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, periodDuration, + return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); } protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale, - long presentationTimeOffset, long periodDuration, int startNumber, long duration, + long presentationTimeOffset, long periodDurationMs, int startNumber, long duration, List timeline, UrlTemplate initializationTemplate, UrlTemplate mediaTemplate, Uri baseUrl) { - return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDuration, + return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java index a7393865f7b..91093980c6 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer.dash.mpd; import com.google.android.exoplayer.C; +import com.google.android.exoplayer.dash.DashSegmentIndex; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -127,17 +128,28 @@ public abstract class SegmentBase { this.segmentTimeline = segmentTimeline; } - public final int getSegmentNum(long timeUs) { - // TODO: Optimize this - int index = startNumber; - while (index + 1 <= getLastSegmentNum()) { - if (getSegmentTimeUs(index + 1) <= timeUs) { - index++; - } else { - return index; + public int getSegmentNum(long timeUs) { + if (segmentTimeline == null) { + // All segments are of equal duration (with the possible exception of the last one). + long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; + return startNumber + (int) (timeUs / durationUs); + } else { + // Identify the segment using binary search. + 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) { @@ -285,6 +297,8 @@ public abstract class SegmentBase { public int getLastSegmentNum() { if (segmentTimeline != null) { return segmentTimeline.size() + startNumber - 1; + } else if (periodDurationMs == -1) { + return DashSegmentIndex.INDEX_UNBOUNDED; } else { long durationMs = (duration * 1000) / timescale; return startNumber + (int) (periodDurationMs / durationMs); diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java index 60c9ae6984..9a2341e904 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java @@ -58,6 +58,7 @@ import java.util.ArrayList; public static final int TYPE_uuid = 0x75756964; public static final int TYPE_senc = 0x73656E63; public static final int TYPE_pasp = 0x70617370; + public static final int TYPE_TTML = 0x54544D4C; public final int type; diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java index 34f0404083..52fe8a94a1 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java @@ -428,7 +428,8 @@ public final class FragmentedMp4Extractor implements Extractor { private static Track parseTrak(ContainerAtom trak) { ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); 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 header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data); int id = header.first; @@ -528,6 +529,8 @@ public final class FragmentedMp4Extractor implements Extractor { parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize); mediaFormat = audioSampleEntry.first; trackEncryptionBoxes[i] = audioSampleEntry.second; + } else if (childAtomType == Atom.TYPE_TTML) { + mediaFormat = MediaFormat.createTtmlFormat(); } stsd.setPosition(childStartPosition + childAtomSize); } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Track.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Track.java index 710626bc2e..a5306c70fa 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Track.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Track.java @@ -30,6 +30,10 @@ public final class Track { * Type of an audio track. */ public static final int TYPE_AUDIO = 0x736F756E; + /** + * Type of a text track. + */ + public static final int TYPE_TEXT = 0x74657874; /** * Type of a hint track. */ diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 936fdf824d..0bc36a0b8b 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -358,8 +358,9 @@ public class SmoothStreamingChunkSource implements ChunkSource { MediaFormat format = MediaFormat.createAudioFormat(mimeType, -1, trackElement.numChannels, trackElement.sampleRate, csd); 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; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 4fd581bf56..c85eb469c1 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -58,8 +58,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { private final TextRenderer textRenderer; private final SampleSource source; private final MediaFormatHolder formatHolder; - private final SubtitleParser subtitleParser; + private final SubtitleParser[] subtitleParsers; + private int parserIndex; private int trackIndex; 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 subtitleParser A subtitle parser that will parse Subtitle objects from the source. * @param textRenderer The text renderer. * @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 * 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 * 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, - TextRenderer textRenderer, Looper textRendererLooper) { + public TextTrackRenderer(SampleSource source, TextRenderer textRenderer, + Looper textRendererLooper, SubtitleParser... subtitleParsers) { this.source = Assertions.checkNotNull(source); - this.subtitleParser = Assertions.checkNotNull(subtitleParser); this.textRenderer = Assertions.checkNotNull(textRenderer); - this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, - this); + this.textRendererHandler = textRendererLooper == null ? null + : new Handler(textRendererLooper, this); + this.subtitleParsers = Assertions.checkNotNull(subtitleParsers); formatHolder = new MediaFormatHolder(); } @@ -101,10 +103,13 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { } catch (IOException e) { throw new ExoPlaybackException(e); } - for (int i = 0; i < source.getTrackCount(); i++) { - if (subtitleParser.canParse(source.getTrackInfo(i).mimeType)) { - trackIndex = i; - return TrackRenderer.STATE_PREPARED; + for (int i = 0; i < subtitleParsers.length; i++) { + for (int j = 0; j < source.getTrackCount(); j++) { + if (subtitleParsers[i].canParse(source.getTrackInfo(j).mimeType)) { + parserIndex = i; + trackIndex = j; + return TrackRenderer.STATE_PREPARED; + } } } return TrackRenderer.STATE_IGNORE; @@ -115,7 +120,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { source.enable(trackIndex, positionUs); parserThread = new HandlerThread("textParser"); parserThread.start(); - parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParser); + parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParsers[parserIndex]); seekToInternal(positionUs); } @@ -189,6 +194,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); if (result == SampleSource.SAMPLE_READ) { parserHelper.startParseOperation(); + textRendererNeedsUpdate = false; } else if (result == SampleSource.END_OF_STREAM) { inputStreamEnded = true; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java index 036c6116a1..0a854fedda 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java @@ -32,7 +32,6 @@ import java.util.regex.Pattern; * A simple WebVTT parser. *

* @see WebVTT specification - *

*/ public class WebvttParser implements SubtitleParser {