From 3e33fddbc195b4329d8bfd06b23d4c1cf462a04d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 12 Dec 2014 13:59:45 +0000 Subject: [PATCH 1/7] Correctly handle audio-only SmoothStreaming streams. --- .../SmoothStreamingRendererBuilder.java | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) 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..b9a857f613 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 @@ -149,19 +149,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; @@ -224,11 +233,6 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, mainHandler.getLooper()); } - // 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; From 099bbe048f338bbd7470e2782e9488fa94946d84 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 12 Dec 2014 14:02:14 +0000 Subject: [PATCH 2/7] Correctly handle audio and video only DASH streams. --- .../demo/full/player/DashRendererBuilder.java | 73 ++++++++++++------- 1 file changed, 46 insertions(+), 27 deletions(-) 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..2e7d36cc65 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 @@ -119,11 +119,27 @@ 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) { @@ -137,7 +153,8 @@ 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 (Exception e) { callback.onRenderersError(e); return; @@ -145,20 +162,22 @@ public class DashRendererBuilder implements RendererBuilder, } // 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 +203,19 @@ 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(); 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)); + } } // Build the audio renderer. From ae55b12bd8455758f23dfffa9e2d007af2ae5383 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 12 Dec 2014 14:05:45 +0000 Subject: [PATCH 3/7] Don't require samples to specify whether they're encrypted. This wasn't actually used for anything other than showing a toast, which is now generated based on the manifest instead. --- .../exoplayer/demo/SampleChooserActivity.java | 7 --- .../android/exoplayer/demo/Samples.java | 47 ++++++++----------- .../demo/full/FullPlayerActivity.java | 12 +++++ .../demo/full/player/DashRendererBuilder.java | 10 ++-- .../SmoothStreamingRendererBuilder.java | 10 ++-- demo/src/main/res/values/strings.xml | 6 ++- 6 files changed, 51 insertions(+), 41 deletions(-) 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 adb28ef0dc..99388aa650 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. @@ -76,11 +74,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 deea767d07..c9ddec33f4 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,19 +43,17 @@ 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("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[] { @@ -66,14 +61,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[] { @@ -81,21 +74,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[] { @@ -103,42 +96,42 @@ 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[] 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 423af3d40e..492c1ab0ae 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 @@ -24,6 +24,7 @@ import com.google.android.exoplayer.demo.full.player.DefaultRendererBuilder; import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder; +import com.google.android.exoplayer.demo.full.player.UnsupportedDrmException; import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.SubtitleView; import com.google.android.exoplayer.util.Util; @@ -52,6 +53,7 @@ import android.widget.MediaController; import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.TextView; +import android.widget.Toast; /** * An activity that plays media using {@link DemoPlayer}. @@ -256,6 +258,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 2e7d36cc65..81a9d0268d 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 @@ -144,8 +144,8 @@ public class DashRendererBuilder implements RendererBuilder, 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 { @@ -155,8 +155,12 @@ public class DashRendererBuilder implements RendererBuilder, // HD streams require L1 security. 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; } } 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 b9a857f613..1a515ba4d2 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; } } 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 From bb024fda088f67cf552017bc3ca4a9cf164a4a87 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 12 Dec 2014 14:07:48 +0000 Subject: [PATCH 4/7] Partial support for DASH DVB Live streams. - Adds support for dash manifests that define SegmentTemplate but no SegmentTimeline. - Assumes that the device clock is correct when calculating which segments to load. The final step here is to use the Utc timing element in the DASH manifest to obtain an accurate client clock. - Doesn't yet enforce that the client shouldn't load segments that are in the future or behind the live window. --- .../exoplayer/dash/DashChunkSource.java | 35 +++++++++++++------ .../exoplayer/dash/DashSegmentIndex.java | 12 +++++-- .../MediaPresentationDescriptionParser.java | 16 ++++----- .../exoplayer/dash/mpd/SegmentBase.java | 32 ++++++++++++----- 4 files changed, 65 insertions(+), 30 deletions(-) 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); From 9d4e1773474579ac1be07ca588ff06bd75dce10d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 12 Dec 2014 14:12:00 +0000 Subject: [PATCH 5/7] Support DASH Live TTML subtitles. Also add missing file. --- .../demo/full/player/DashRendererBuilder.java | 5 ++- .../SmoothStreamingRendererBuilder.java | 4 +- .../full/player/UnsupportedDrmException.java | 38 +++++++++++++++++++ .../google/android/exoplayer/MediaFormat.java | 10 +++++ .../exoplayer/chunk/ChunkSampleSource.java | 16 ++++++-- .../android/exoplayer/parser/mp4/Atom.java | 1 + .../parser/mp4/FragmentedMp4Extractor.java | 5 ++- .../android/exoplayer/parser/mp4/Track.java | 4 ++ .../SmoothStreamingChunkSource.java | 3 +- .../exoplayer/text/TextTrackRenderer.java | 30 +++++++++------ 10 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 demo/src/main/java/com/google/android/exoplayer/demo/full/player/UnsupportedDrmException.java 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 81a9d0268d..907175aea6 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 @@ -41,6 +41,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; @@ -274,8 +275,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/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index 1a515ba4d2..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 @@ -233,8 +233,8 @@ 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()); } // Invoke the callback. 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/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index 24db47ff77..20a45738bc 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer; +import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; import android.annotation.SuppressLint; @@ -86,6 +87,15 @@ public class MediaFormat { sampleRate, bitrate, initializationData); } + 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) private MediaFormat(android.media.MediaFormat format) { this.frameworkMediaFormat = format; 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/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; } From 81bf68b1cb9e689c24ebc38e47f98ee71fc4a143 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 12 Dec 2014 14:13:58 +0000 Subject: [PATCH 6/7] Minor doc fixes. --- .../java/com/google/android/exoplayer/TrackRenderer.java | 6 ++---- .../google/android/exoplayer/text/webvtt/WebvttParser.java | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) 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/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 { From 5a3340d638cfeb3809f435b1b0118f2acce15079 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 12 Dec 2014 14:18:44 +0000 Subject: [PATCH 7/7] Add initial AC3 passthrough support. --- .../demo/full/FullPlayerActivity.java | 28 +- .../demo/full/player/DashRendererBuilder.java | 32 +- .../demo/full/player/DemoPlayer.java | 5 +- .../Ac3PassthroughAudioTrackRenderer.java | 316 ++++++++++++++++++ 4 files changed, 373 insertions(+), 8 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java 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 492c1ab0ae..d32c61fd22 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; @@ -59,7 +61,7 @@ import android.widget.Toast; * An activity that plays media using {@link DemoPlayer}. */ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, - DemoPlayer.Listener, DemoPlayer.TextListener { + DemoPlayer.Listener, DemoPlayer.TextListener, AudioCapabilitiesReceiver.Listener { private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f; private static final int MENU_GROUP_TRACKS = 1; @@ -89,6 +91,9 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba private int contentType; private String contentId; + private AudioCapabilitiesReceiver audioCapabilitiesReceiver; + private AudioCapabilities audioCapabilities; + // Activity lifecycle @Override @@ -112,6 +117,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); @@ -137,7 +144,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 @@ -148,6 +157,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } else { player.blockingClearSurface(); } + + audioCapabilitiesReceiver.unregister(); } @Override @@ -166,6 +177,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() { @@ -176,7 +198,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); default: return new DefaultRendererBuilder(this, contentUri, debugTextView); } 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 907175aea6..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; @@ -84,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 @@ -208,6 +212,7 @@ public class DashRendererBuilder implements RendererBuilder, } // Build the audio chunk sources. + boolean haveAc3Tracks = false; List audioChunkSourceList = new ArrayList(); List audioTrackNameList = new ArrayList(); if (audioAdaptationSet != null) { @@ -220,6 +225,19 @@ public class DashRendererBuilder implements RendererBuilder, 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); + } + } } } @@ -238,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. 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 dfa900d18d..97ce5f5506 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; @@ -45,8 +46,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/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); + } + }); + } + } + +}