diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..09cc6bdf415 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# How to contribute # + +We'd love to hear your feedback. Please open new issues describing any bugs, +feature requests or suggestions that you have. + +We are not actively looking to accept patches to this project at the current +time, however in some cases we may do so. For such cases, please see the +agreement below. + + +## Contributor License Agreement ## + +Contributions to any Google project must be accompanied by a Contributor +License Agreement. This is not a copyright **assignment**, it simply gives +Google permission to use and redistribute your contributions as part of the +project. + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual + CLA][]. + + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA][]. + +You generally only need to submit a CLA once, so if you've already submitted +one (even if it was for a different project), you probably don't need to do it +again. + +[individual CLA]: https://developers.google.com/open-source/cla/individual +[corporate CLA]: https://developers.google.com/open-source/cla/corporate diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 00000000000..389ead7b177 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# ExoPlayer Readme # + +## Description ## + +ExoPlayer is an application level media player for Android. It provides an +alternative to Android’s MediaPlayer API for playing audio and video both +locally and over the internet. ExoPlayer supports features not currently +supported by Android’s MediaPlayer API (as of KitKat), including DASH and +SmoothStreaming adaptive playbacks, persistent caching and custom renderers. +Unlike the MediaPlayer API, ExoPlayer is easy to customize and extend, and +can be updated through Play Store application updates. + + +## Developer guide ## + +The [ExoPlayer developer guide][] provides a wealth of information to help you +get started. + +[ExoPlayer developer guide]: http://developer.android.com/guide/topics/media/exoplayer.html + + +## Using Eclipse ## + +The repository includes Eclipse projects for both the ExoPlayer library and its +accompanying demo application. To get started: + + 1. Install Eclipse and setup the [Android SDK][]. + + 1. Open Eclipse and navigate to File->Import->General->Existing Projects into + Workspace. + + 1. Select the root directory of the repository. + + 1. Import the ExoPlayerDemo and ExoPlayerLib projects. + +[Android SDK]: http://developer.android.com/sdk/index.html + + +## Using Gradle ## + +ExoPlayer can also be built using Gradle. For a complete list of tasks, run: + +./gradlew tasks diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000000..2d29f854be8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,30 @@ +// 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. + +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.10.+' + } +} + +allprojects { + repositories { + mavenCentral() + } +} diff --git a/demo/build.gradle b/demo/build.gradle new file mode 100644 index 00000000000..c2916dd0f6d --- /dev/null +++ b/demo/build.gradle @@ -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. +apply plugin: 'android' + +android { + compileSdkVersion 19 + buildToolsVersion "19.1" + + defaultConfig { + minSdkVersion 9 + targetSdkVersion 19 + } + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } + + lintOptions { + abortOnError false + } +} + +dependencies { + compile project(':library') +} diff --git a/demo/src/main/.classpath b/demo/src/main/.classpath new file mode 100644 index 00000000000..846a9fbeccc --- /dev/null +++ b/demo/src/main/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/demo/src/main/.project b/demo/src/main/.project new file mode 100644 index 00000000000..ff9802eb2f0 --- /dev/null +++ b/demo/src/main/.project @@ -0,0 +1,53 @@ + + + ExoPlayerDemo + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + + + 1363908154650 + + 22 + + org.eclipse.ui.ide.multiFilter + 1.0-name-matches-false-false-BUILD + + + + 1363908154652 + + 10 + + org.eclipse.ui.ide.multiFilter + 1.0-name-matches-true-false-build + + + + diff --git a/demo/src/main/.settings/org.eclipse.jdt.core.prefs b/demo/src/main/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..b080d2ddc88 --- /dev/null +++ b/demo/src/main/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..330adc6d2b6 --- /dev/null +++ b/demo/src/main/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java new file mode 100644 index 00000000000..f8ceebc6004 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java @@ -0,0 +1,108 @@ +/* + * 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; + +import com.google.android.exoplayer.ExoPlayerLibraryInfo; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.UUID; + +/** + * Utility methods for the demo application. + */ +public class DemoUtil { + + public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); + + public static final String CONTENT_TYPE_EXTRA = "content_type"; + public static final String CONTENT_ID_EXTRA = "content_id"; + + public static final int TYPE_DASH_VOD = 0; + public static final int TYPE_SS_VOD = 1; + public static final int TYPE_OTHER = 2; + + public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false; + + public static String getUserAgent(Context context) { + String versionName; + try { + String packageName = context.getPackageName(); + PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); + versionName = info.versionName; + } catch (NameNotFoundException e) { + versionName = "?"; + } + return "ExoPlayerDemo/" + versionName + " (Linux;Android " + Build.VERSION.RELEASE + + ") " + "ExoPlayerLib/" + ExoPlayerLibraryInfo.VERSION; + } + + public static byte[] executePost(String url, byte[] data, Map requestProperties) + throws MalformedURLException, IOException { + HttpURLConnection urlConnection = null; + try { + urlConnection = (HttpURLConnection) new URL(url).openConnection(); + urlConnection.setRequestMethod("POST"); + urlConnection.setDoOutput(data != null); + urlConnection.setDoInput(true); + if (requestProperties != null) { + for (Map.Entry requestProperty : requestProperties.entrySet()) { + urlConnection.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); + } + } + if (data != null) { + OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream()); + out.write(data); + out.close(); + } + InputStream in = new BufferedInputStream(urlConnection.getInputStream()); + return convertInputStreamToByteArray(in); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } + + private static byte[] convertInputStreamToByteArray(InputStream inputStream) throws IOException { + byte[] bytes = null; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte data[] = new byte[1024]; + int count; + while ((count = inputStream.read(data)) != -1) { + bos.write(data, 0, count); + } + bos.flush(); + bos.close(); + inputStream.close(); + bytes = bos.toByteArray(); + return bytes; + } + +} 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 new file mode 100644 index 00000000000..519a252c797 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java @@ -0,0 +1,140 @@ +/* + * 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; + +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; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +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. + */ +public class SampleChooserActivity extends Activity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.sample_chooser_activity); + + ListView sampleList = (ListView) findViewById(R.id.sample_list); + final SampleAdapter sampleAdapter = new SampleAdapter(this); + + sampleAdapter.add(new Header("Simple player")); + sampleAdapter.addAll((Object[]) Samples.SIMPLE); + sampleAdapter.add(new Header("YouTube DASH")); + sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_MP4); + sampleAdapter.add(new Header("Widevine DASH GTS")); + sampleAdapter.addAll((Object[]) Samples.WIDEVINE_GTS); + sampleAdapter.add(new Header("SmoothStreaming")); + sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING); + sampleAdapter.add(new Header("Misc")); + sampleAdapter.addAll((Object[]) Samples.MISC); + if (DemoUtil.EXPOSE_EXPERIMENTAL_FEATURES) { + sampleAdapter.add(new Header("YouTube WebM DASH (Experimental)")); + sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_WEBM); + } + + sampleList.setAdapter(sampleAdapter); + sampleList.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Object item = sampleAdapter.getItem(position); + if (item instanceof Sample) { + onSampleSelected((Sample) item); + } + } + }); + } + + 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) + .setData(Uri.parse(sample.uri)) + .putExtra(DemoUtil.CONTENT_ID_EXTRA, sample.contentId) + .putExtra(DemoUtil.CONTENT_TYPE_EXTRA, sample.type); + startActivity(mpdIntent); + } + + private static class SampleAdapter extends ArrayAdapter { + + public SampleAdapter(Context context) { + super(context, 0); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + int layoutId = getItemViewType(position) == 1 ? android.R.layout.simple_list_item_1 + : R.layout.sample_chooser_inline_header; + view = LayoutInflater.from(getContext()).inflate(layoutId, null, false); + } + Object item = getItem(position); + String name = null; + if (item instanceof Sample) { + name = ((Sample) item).name; + } else if (item instanceof Header) { + name = ((Header) item).name; + } + ((TextView) view).setText(name); + return view; + } + + @Override + public int getItemViewType(int position) { + return (getItem(position) instanceof Sample) ? 1 : 0; + } + + @Override + public int getViewTypeCount() { + return 2; + } + + } + + private static class Header { + + public final String name; + + public Header(String name) { + this.name = name; + } + + } + +} 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 new file mode 100644 index 00000000000..c2dc0ff1c76 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -0,0 +1,141 @@ +/* + * 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; + +/** + * Holds statically defined sample definitions. + */ +/* package */ class Samples { + + public static class Sample { + + public final String name; + 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) { + this.name = name; + this.contentId = contentId; + this.uri = uri; + this.type = type; + this.isEncypted = isEncrypted; + this.fullPlayer = fullPlayer; + } + + } + + public static final Sample[] SIMPLE = new Sample[] { + new Sample("Google Glass (DASH)", "bf5bb2419360daf1", + "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_VOD, false, + 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_VOD, false, + false), + new Sample("Super speed (SmoothStreaming)", "uid:ss:superspeed", + "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism", + DemoUtil.TYPE_SS_VOD, false, false), + new Sample("Dizzy (Misc)", "uid:misc:dizzy", + "http://html5demos.com/assets/dizzy.mp4", DemoUtil.TYPE_OTHER, false, false), + }; + + public static final Sample[] YOUTUBE_DASH_MP4 = new Sample[] { + new Sample("Google Glass", "bf5bb2419360daf1", + "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_VOD, false, + 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_VOD, false, + true), + }; + + public static final Sample[] YOUTUBE_DASH_WEBM = new Sample[] { + new Sample("Google Glass", "bf5bb2419360daf1", + "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_VOD, false, 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_VOD, false, 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_VOD, false, true), + new Sample("Super speed (PlayReady)", "uid:ss:pr:superspeed", + "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism", + DemoUtil.TYPE_SS_VOD, true, true), + }; + + public static final Sample[] WIDEVINE_GTS = new Sample[] { + new Sample("WV: HDCP not specified", "d286538032258a1c", + "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_VOD, true, 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_VOD, true, 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_VOD, true, 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_VOD, true, 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_VOD, true, 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_VOD, true, 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), + }; + + private Samples() {} + +} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java new file mode 100644 index 00000000000..7db4240d420 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/EventLogger.java @@ -0,0 +1,190 @@ +/* + * 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; + +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer.AudioTrackInitializationException; +import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; +import com.google.android.exoplayer.demo.full.player.DemoPlayer; +import com.google.android.exoplayer.util.VerboseLogUtil; + +import android.media.MediaCodec.CryptoException; +import android.os.SystemClock; +import android.util.Log; + +import java.io.IOException; +import java.text.NumberFormat; +import java.util.Locale; + +/** + * Logs player events using {@link Log}. + */ +public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener, + DemoPlayer.InternalErrorListener { + + private static final String TAG = "EventLogger"; + private static final NumberFormat TIME_FORMAT; + static { + TIME_FORMAT = NumberFormat.getInstance(Locale.US); + TIME_FORMAT.setMinimumFractionDigits(2); + TIME_FORMAT.setMaximumFractionDigits(2); + } + + private long sessionStartTimeMs; + private long[] loadStartTimeMs; + + public EventLogger() { + loadStartTimeMs = new long[DemoPlayer.RENDERER_COUNT]; + } + + public void startSession() { + sessionStartTimeMs = SystemClock.elapsedRealtime(); + Log.d(TAG, "start [0]"); + } + + public void endSession() { + Log.d(TAG, "end [" + getSessionTimeString() + "]"); + } + + // DemoPlayer.Listener + + @Override + public void onStateChanged(boolean playWhenReady, int state) { + Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", " + + getStateString(state) + "]"); + } + + @Override + public void onError(Exception e) { + Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e); + } + + @Override + public void onVideoSizeChanged(int width, int height) { + Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]"); + } + + // DemoPlayer.InfoListener + + @Override + public void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate) { + Log.d(TAG, "bandwidth [" + getSessionTimeString() + ", " + bytes + + ", " + getTimeString(elapsedMs) + ", " + bandwidthEstimate + "]"); + } + + @Override + public void onDroppedFrames(int count, long elapsed) { + Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]"); + } + + @Override + public void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, + int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) { + loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime(); + if (VerboseLogUtil.isTagEnabled(TAG)) { + Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId + + ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]"); + } + } + + @Override + public void onLoadCompleted(int sourceId) { + if (VerboseLogUtil.isTagEnabled(TAG)) { + long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId]; + Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " + + downloadTime + "]"); + } + } + + @Override + public void onVideoFormatEnabled(int formatId, int trigger, int mediaTimeMs) { + Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + formatId + ", " + + Integer.toString(trigger) + "]"); + } + + @Override + public void onAudioFormatEnabled(int formatId, int trigger, int mediaTimeMs) { + Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + formatId + ", " + + Integer.toString(trigger) + "]"); + } + + // DemoPlayer.InternalErrorListener + + @Override + public void onUpstreamError(int sourceId, IOException e) { + printInternalError("upstreamError", e); + } + + @Override + public void onConsumptionError(int sourceId, IOException e) { + printInternalError("consumptionError", e); + } + + @Override + public void onRendererInitializationError(Exception e) { + printInternalError("rendererInitError", e); + } + + @Override + public void onDrmSessionManagerError(Exception e) { + printInternalError("drmSessionManagerError", e); + } + + @Override + public void onDecoderInitializationError(DecoderInitializationException e) { + printInternalError("decoderInitializationError", e); + } + + @Override + public void onAudioTrackInitializationError(AudioTrackInitializationException e) { + printInternalError("audioTrackInitializationError", e); + } + + @Override + public void onCryptoError(CryptoException e) { + printInternalError("cryptoError", e); + } + + private void printInternalError(String type, Exception e) { + Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); + } + + private String getStateString(int state) { + switch (state) { + case ExoPlayer.STATE_BUFFERING: + return "B"; + case ExoPlayer.STATE_ENDED: + return "E"; + case ExoPlayer.STATE_IDLE: + return "I"; + case ExoPlayer.STATE_PREPARING: + return "P"; + case ExoPlayer.STATE_READY: + return "R"; + default: + return "?"; + } + } + + private String getSessionTimeString() { + return getTimeString(SystemClock.elapsedRealtime() - sessionStartTimeMs); + } + + private String getTimeString(long timeMs) { + return TIME_FORMAT.format((timeMs) / 1000f); + } + +} 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 new file mode 100644 index 00000000000..da8c1cd2498 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -0,0 +1,412 @@ +/* + * 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; + +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.VideoSurfaceView; +import com.google.android.exoplayer.demo.DemoUtil; +import com.google.android.exoplayer.demo.R; +import com.google.android.exoplayer.demo.full.player.DashVodRendererBuilder; +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.util.VerboseLogUtil; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnTouchListener; +import android.widget.Button; +import android.widget.MediaController; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; +import android.widget.TextView; + +/** + * An activity that plays media using {@link DemoPlayer}. + */ +public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, + DemoPlayer.Listener, DemoPlayer.TextListener { + + private static final int MENU_GROUP_TRACKS = 1; + private static final int ID_OFFSET = 2; + + private EventLogger eventLogger; + private MediaController mediaController; + private View debugRootView; + private View shutterView; + private VideoSurfaceView surfaceView; + private TextView debugTextView; + private TextView playerStateTextView; + private TextView subtitlesTextView; + private Button videoButton; + private Button audioButton; + private Button textButton; + private Button retryButton; + + private DemoPlayer player; + private boolean playerNeedsPrepare; + + private boolean autoPlay = true; + private int playerPosition; + private boolean enableBackgroundAudio = false; + + private Uri contentUri; + private int contentType; + private String contentId; + + // Activity lifecycle + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + contentUri = intent.getData(); + contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, DemoUtil.TYPE_OTHER); + contentId = intent.getStringExtra(DemoUtil.CONTENT_ID_EXTRA); + + setContentView(R.layout.player_activity_full); + View root = findViewById(R.id.root); + root.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View arg0, MotionEvent arg1) { + if (arg1.getAction() == MotionEvent.ACTION_DOWN) { + toggleControlsVisibility(); + } + return true; + } + }); + + shutterView = findViewById(R.id.shutter); + debugRootView = findViewById(R.id.controls_root); + + surfaceView = (VideoSurfaceView) findViewById(R.id.surface_view); + surfaceView.getHolder().addCallback(this); + debugTextView = (TextView) findViewById(R.id.debug_text_view); + + playerStateTextView = (TextView) findViewById(R.id.player_state_view); + subtitlesTextView = (TextView) findViewById(R.id.subtitles); + + mediaController = new MediaController(this); + mediaController.setAnchorView(root); + retryButton = (Button) findViewById(R.id.retry_button); + retryButton.setOnClickListener(this); + videoButton = (Button) findViewById(R.id.video_controls); + audioButton = (Button) findViewById(R.id.audio_controls); + textButton = (Button) findViewById(R.id.text_controls); + } + + @Override + public void onResume() { + super.onResume(); + preparePlayer(); + } + + @Override + public void onPause() { + super.onPause(); + if (!enableBackgroundAudio) { + releasePlayer(); + } else { + player.blockingClearSurface(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + releasePlayer(); + } + + // OnClickListener methods + + @Override + public void onClick(View view) { + if (view == retryButton) { + autoPlay = true; + preparePlayer(); + } + } + + // Internal methods + + private RendererBuilder getRendererBuilder() { + String userAgent = DemoUtil.getUserAgent(this); + switch (contentType) { + case DemoUtil.TYPE_SS_VOD: + return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), contentId, + new SmoothStreamingTestMediaDrmCallback(), debugTextView); + case DemoUtil.TYPE_DASH_VOD: + return new DashVodRendererBuilder(userAgent, contentUri.toString(), contentId, + new WidevineTestMediaDrmCallback(contentId), debugTextView); + default: + return new DefaultRendererBuilder(this, contentUri, debugTextView); + } + } + + private void preparePlayer() { + if (player == null) { + player = new DemoPlayer(getRendererBuilder()); + player.addListener(this); + player.setTextListener(this); + player.seekTo(playerPosition); + playerNeedsPrepare = true; + mediaController.setMediaPlayer(player.getPlayerControl()); + mediaController.setEnabled(true); + eventLogger = new EventLogger(); + eventLogger.startSession(); + player.addListener(eventLogger); + player.setInfoListener(eventLogger); + player.setInternalErrorListener(eventLogger); + } + if (playerNeedsPrepare) { + player.prepare(); + playerNeedsPrepare = false; + updateButtonVisibilities(); + } + player.setSurface(surfaceView.getHolder().getSurface()); + maybeStartPlayback(); + } + + private void maybeStartPlayback() { + if (autoPlay && (player.getSurface().isValid() + || player.getSelectedTrackIndex(DemoPlayer.TYPE_VIDEO) == DemoPlayer.DISABLED_TRACK)) { + player.setPlayWhenReady(true); + autoPlay = false; + } + } + + private void releasePlayer() { + if (player != null) { + playerPosition = player.getCurrentPosition(); + player.release(); + player = null; + eventLogger.endSession(); + eventLogger = null; + } + } + + // DemoPlayer.Listener implementation + + @Override + public void onStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == ExoPlayer.STATE_ENDED) { + showControls(); + } + String text = "playWhenReady=" + playWhenReady + ", playbackState="; + switch(playbackState) { + case ExoPlayer.STATE_BUFFERING: + text += "buffering"; + break; + case ExoPlayer.STATE_ENDED: + text += "ended"; + break; + case ExoPlayer.STATE_IDLE: + text += "idle"; + break; + case ExoPlayer.STATE_PREPARING: + text += "preparing"; + break; + case ExoPlayer.STATE_READY: + text += "ready"; + break; + default: + text += "unknown"; + break; + } + playerStateTextView.setText(text); + updateButtonVisibilities(); + } + + @Override + public void onError(Exception e) { + playerNeedsPrepare = true; + updateButtonVisibilities(); + showControls(); + } + + @Override + public void onVideoSizeChanged(int width, int height) { + shutterView.setVisibility(View.GONE); + surfaceView.setVideoWidthHeightRatio(height == 0 ? 1 : (float) width / height); + } + + // User controls + + private void updateButtonVisibilities() { + retryButton.setVisibility(playerNeedsPrepare ? View.VISIBLE : View.GONE); + videoButton.setVisibility(haveTracks(DemoPlayer.TYPE_VIDEO) ? View.VISIBLE : View.GONE); + audioButton.setVisibility(haveTracks(DemoPlayer.TYPE_AUDIO) ? View.VISIBLE : View.GONE); + textButton.setVisibility(haveTracks(DemoPlayer.TYPE_TEXT) ? View.VISIBLE : View.GONE); + } + + private boolean haveTracks(int type) { + return player != null && player.getTracks(type) != null; + } + + public void showVideoPopup(View v) { + PopupMenu popup = new PopupMenu(this, v); + configurePopupWithTracks(popup, null, DemoPlayer.TYPE_VIDEO); + popup.show(); + } + + public void showAudioPopup(View v) { + PopupMenu popup = new PopupMenu(this, v); + Menu menu = popup.getMenu(); + menu.add(Menu.NONE, Menu.NONE, Menu.NONE, R.string.enable_background_audio); + final MenuItem backgroundAudioItem = menu.findItem(0); + backgroundAudioItem.setCheckable(true); + backgroundAudioItem.setChecked(enableBackgroundAudio); + OnMenuItemClickListener clickListener = new OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item == backgroundAudioItem) { + enableBackgroundAudio = !item.isChecked(); + return true; + } + return false; + } + }; + configurePopupWithTracks(popup, clickListener, DemoPlayer.TYPE_AUDIO); + popup.show(); + } + + public void showTextPopup(View v) { + PopupMenu popup = new PopupMenu(this, v); + configurePopupWithTracks(popup, null, DemoPlayer.TYPE_TEXT); + popup.show(); + } + + public void showVerboseLogPopup(View v) { + PopupMenu popup = new PopupMenu(this, v); + Menu menu = popup.getMenu(); + menu.add(Menu.NONE, 0, Menu.NONE, R.string.logging_normal); + menu.add(Menu.NONE, 1, Menu.NONE, R.string.logging_verbose); + menu.setGroupCheckable(Menu.NONE, true, true); + menu.findItem((VerboseLogUtil.areAllTagsEnabled()) ? 1 : 0).setChecked(true); + popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == 0) { + VerboseLogUtil.setEnableAllTags(false); + } else { + VerboseLogUtil.setEnableAllTags(true); + } + return true; + } + }); + popup.show(); + } + + private void configurePopupWithTracks(PopupMenu popup, + final OnMenuItemClickListener customActionClickListener, + final int trackType) { + if (player == null) { + return; + } + String[] tracks = player.getTracks(trackType); + if (tracks == null) { + return; + } + popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + return (customActionClickListener != null + && customActionClickListener.onMenuItemClick(item)) + || onTrackItemClick(item, trackType); + } + }); + Menu menu = popup.getMenu(); + // ID_OFFSET ensures we avoid clashing with Menu.NONE (which equals 0) + menu.add(MENU_GROUP_TRACKS, DemoPlayer.DISABLED_TRACK + ID_OFFSET, Menu.NONE, R.string.off); + if (tracks.length == 1 && TextUtils.isEmpty(tracks[0])) { + menu.add(MENU_GROUP_TRACKS, DemoPlayer.PRIMARY_TRACK + ID_OFFSET, Menu.NONE, R.string.on); + } else { + for (int i = 0; i < tracks.length; i++) { + menu.add(MENU_GROUP_TRACKS, i + ID_OFFSET, Menu.NONE, tracks[i]); + } + } + menu.setGroupCheckable(MENU_GROUP_TRACKS, true, true); + menu.findItem(player.getSelectedTrackIndex(trackType) + ID_OFFSET).setChecked(true); + } + + private boolean onTrackItemClick(MenuItem item, int type) { + if (player == null || item.getGroupId() != MENU_GROUP_TRACKS) { + return false; + } + player.selectTrack(type, item.getItemId() - ID_OFFSET); + return true; + } + + private void toggleControlsVisibility() { + if (mediaController.isShowing()) { + mediaController.hide(); + debugRootView.setVisibility(View.GONE); + } else { + showControls(); + } + } + + private void showControls() { + mediaController.show(0); + debugRootView.setVisibility(View.VISIBLE); + } + + // DemoPlayer.TextListener implementation + + @Override + public void onText(String text) { + if (TextUtils.isEmpty(text)) { + subtitlesTextView.setVisibility(View.INVISIBLE); + } else { + subtitlesTextView.setVisibility(View.VISIBLE); + subtitlesTextView.setText(text); + } + } + + // SurfaceHolder.Callback implementation + + @Override + public void surfaceCreated(SurfaceHolder holder) { + if (player != null) { + player.setSurface(holder.getSurface()); + maybeStartPlayback(); + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + // Do nothing. + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (player != null) { + player.blockingClearSurface(); + } + } + +} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/SmoothStreamingTestMediaDrmCallback.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/SmoothStreamingTestMediaDrmCallback.java new file mode 100644 index 00000000000..b193860423b --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/SmoothStreamingTestMediaDrmCallback.java @@ -0,0 +1,64 @@ +/* + * 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; + +import com.google.android.exoplayer.demo.DemoUtil; +import com.google.android.exoplayer.drm.MediaDrmCallback; +import com.google.android.exoplayer.drm.StreamingDrmSessionManager; + +import android.annotation.TargetApi; +import android.media.MediaDrm.KeyRequest; +import android.media.MediaDrm.ProvisionRequest; +import android.text.TextUtils; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Demo {@link StreamingDrmSessionManager} for smooth streaming test content. + */ +@TargetApi(18) +public class SmoothStreamingTestMediaDrmCallback implements MediaDrmCallback { + + private static final String PLAYREADY_TEST_DEFAULT_URI = + "http://playready.directtaps.net/pr/svc/rightsmanager.asmx"; + private static final Map KEY_REQUEST_PROPERTIES; + static { + HashMap keyRequestProperties = new HashMap(); + keyRequestProperties.put("Content-Type", "text/xml"); + keyRequestProperties.put("SOAPAction", + "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"); + KEY_REQUEST_PROPERTIES = keyRequestProperties; + } + + @Override + public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { + String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); + return DemoUtil.executePost(url, null, null); + } + + @Override + public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { + String url = request.getDefaultUrl(); + if (TextUtils.isEmpty(url)) { + url = PLAYREADY_TEST_DEFAULT_URI; + } + return DemoUtil.executePost(url, request.getData(), KEY_REQUEST_PROPERTIES); + } + +} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/WidevineTestMediaDrmCallback.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/WidevineTestMediaDrmCallback.java new file mode 100644 index 00000000000..f2425589dbe --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/WidevineTestMediaDrmCallback.java @@ -0,0 +1,62 @@ +/* + * 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; + +import com.google.android.exoplayer.demo.DemoUtil; +import com.google.android.exoplayer.drm.MediaDrmCallback; + +import android.annotation.TargetApi; +import android.media.MediaDrm.KeyRequest; +import android.media.MediaDrm.ProvisionRequest; +import android.text.TextUtils; + +import org.apache.http.client.ClientProtocolException; + +import java.io.IOException; +import java.util.UUID; + +/** + * A {@link MediaDrmCallback} for Widevine test content. + */ +@TargetApi(18) +public class WidevineTestMediaDrmCallback implements MediaDrmCallback { + + private static final String WIDEVINE_GTS_DEFAULT_BASE_URI = + "http://wv-staging-proxy.appspot.com/proxy?provider=YouTube&video_id="; + + private final String defaultUri; + + public WidevineTestMediaDrmCallback(String videoId) { + defaultUri = WIDEVINE_GTS_DEFAULT_BASE_URI + videoId; + } + + @Override + public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) + throws ClientProtocolException, IOException { + String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); + return DemoUtil.executePost(url, null, null); + } + + @Override + public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws IOException { + String url = request.getDefaultUrl(); + if (TextUtils.isEmpty(url)) { + url = defaultUri; + } + return DemoUtil.executePost(url, request.getData(), null); + } + +} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java new file mode 100644 index 00000000000..e892b3e27e9 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java @@ -0,0 +1,267 @@ +/* + * 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; + +import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +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.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.ChunkSource; +import com.google.android.exoplayer.chunk.Format; +import com.google.android.exoplayer.chunk.FormatEvaluator; +import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; +import com.google.android.exoplayer.chunk.MultiTrackChunkSource; +import com.google.android.exoplayer.dash.DashMp4ChunkSource; +import com.google.android.exoplayer.dash.DashWebmChunkSource; +import com.google.android.exoplayer.dash.mpd.AdaptationSet; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher; +import com.google.android.exoplayer.dash.mpd.Period; +import com.google.android.exoplayer.dash.mpd.Representation; +import com.google.android.exoplayer.demo.DemoUtil; +import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; +import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback; +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.upstream.BufferPool; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.UnsupportedSchemeException; +import android.os.AsyncTask; +import android.os.Handler; +import android.util.Pair; +import android.widget.TextView; + +import java.util.ArrayList; + +/** + * A {@link RendererBuilder} for DASH VOD. + */ +public class DashVodRendererBuilder implements RendererBuilder, + ManifestCallback { + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int VIDEO_BUFFER_SEGMENTS = 200; + private static final int AUDIO_BUFFER_SEGMENTS = 60; + + private static final int SECURITY_LEVEL_UNKNOWN = -1; + private static final int SECURITY_LEVEL_1 = 1; + private static final int SECURITY_LEVEL_3 = 3; + + private final String userAgent; + private final String url; + private final String contentId; + private final MediaDrmCallback drmCallback; + private final TextView debugTextView; + + private DemoPlayer player; + private RendererBuilderCallback callback; + + public DashVodRendererBuilder(String userAgent, String url, String contentId, + MediaDrmCallback drmCallback, TextView debugTextView) { + this.userAgent = userAgent; + this.url = url; + this.contentId = contentId; + this.drmCallback = drmCallback; + this.debugTextView = debugTextView; + } + + @Override + public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { + this.player = player; + this.callback = callback; + MediaPresentationDescriptionFetcher mpdFetcher = new MediaPresentationDescriptionFetcher(this); + mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId); + } + + @Override + public void onManifestError(String contentId, Exception e) { + callback.onRenderersError(e); + } + + @Override + public void onManifest(String contentId, MediaPresentationDescription manifest) { + Handler mainHandler = player.getMainHandler(); + LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); + DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); + + // Obtain Representations for playback. + int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); + ArrayList audioRepresentationsList = new ArrayList(); + ArrayList videoRepresentationsList = new ArrayList(); + Period period = manifest.periods.get(0); + boolean hasContentProtection = false; + for (int i = 0; i < period.adaptationSets.size(); i++) { + AdaptationSet adaptationSet = period.adaptationSets.get(i); + hasContentProtection |= adaptationSet.hasContentProtection(); + int adaptationSetType = adaptationSet.type; + for (int j = 0; j < adaptationSet.representations.size(); j++) { + Representation representation = adaptationSet.representations.get(j); + if (adaptationSetType == AdaptationSet.TYPE_AUDIO) { + audioRepresentationsList.add(representation); + } else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { + Format format = representation.format; + if (format.width * format.height <= maxDecodableFrameSize) { + videoRepresentationsList.add(representation); + } else { + // The device isn't capable of playing this stream. + } + } + } + } + Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()]; + videoRepresentationsList.toArray(videoRepresentations); + + // Check drm support if necessary. + DrmSessionManager drmSessionManager = null; + if (hasContentProtection) { + if (Util.SDK_INT < 18) { + callback.onRenderersError(new UnsupportedOperationException( + "Protected content not supported on API level " + Util.SDK_INT)); + return; + } + try { + Pair drmSessionManagerData = + V18Compat.getDrmSessionManagerData(player, drmCallback); + drmSessionManager = drmSessionManagerData.first; + if (!drmSessionManagerData.second) { + // HD streams require L1 security. + videoRepresentations = getSdRepresentations(videoRepresentations); + } + } catch (UnsupportedSchemeException e) { + callback.onRenderersError(e); + return; + } + } + + // Build the video renderer. + DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, + bandwidthMeter); + ChunkSource videoChunkSource; + String mimeType = videoRepresentations[0].format.mimeType; + if (mimeType.equals(MimeTypes.VIDEO_MP4)) { + videoChunkSource = new DashMp4ChunkSource(videoDataSource, + new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); + } else if (mimeType.equals(MimeTypes.VIDEO_WEBM)) { + // TODO: Figure out how to query supported vpX resolutions. For now, restrict to standard + // definition streams. + videoRepresentations = getSdRepresentations(videoRepresentations); + videoChunkSource = new DashWebmChunkSource(videoDataSource, + new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); + } else { + throw new IllegalStateException("Unexpected mime type: " + mimeType); + } + 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, + mainHandler, player, 50); + + // Build the audio renderer. + final String[] audioTrackNames; + final MultiTrackChunkSource audioChunkSource; + final MediaCodecAudioTrackRenderer audioRenderer; + if (audioRepresentationsList.isEmpty()) { + audioTrackNames = null; + audioChunkSource = null; + audioRenderer = null; + } else { + DataSource audioDataSource = new HttpDataSource(userAgent, + HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter); + audioTrackNames = new String[audioRepresentationsList.size()]; + ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()]; + FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); + for (int i = 0; i < audioRepresentationsList.size(); i++) { + Representation representation = audioRepresentationsList.get(i); + Format format = representation.format; + audioTrackNames[i] = format.id + " (" + format.numChannels + "ch, " + + format.audioSamplingRate + "Hz)"; + audioChunkSources[i] = new DashMp4ChunkSource(audioDataSource, + audioEvaluator, representation); + } + audioChunkSource = new MultiTrackChunkSource(audioChunkSources); + 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); + } + + // 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; + + MultiTrackChunkSource[] multiTrackChunkSources = + new MultiTrackChunkSource[DemoPlayer.RENDERER_COUNT]; + multiTrackChunkSources[DemoPlayer.TYPE_AUDIO] = audioChunkSource; + + TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; + renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; + renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; + renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; + callback.onRenderers(trackNames, multiTrackChunkSources, renderers); + } + + private Representation[] getSdRepresentations(Representation[] representations) { + ArrayList sdRepresentations = new ArrayList(); + for (int i = 0; i < representations.length; i++) { + if (representations[i].format.height < 720 && representations[i].format.width < 1280) { + sdRepresentations.add(representations[i]); + } + } + Representation[] sdRepresentationArray = new Representation[sdRepresentations.size()]; + sdRepresentations.toArray(sdRepresentationArray); + return sdRepresentationArray; + } + + @TargetApi(18) + private static class V18Compat { + + public static Pair getDrmSessionManagerData(DemoPlayer player, + MediaDrmCallback drmCallback) throws UnsupportedSchemeException { + StreamingDrmSessionManager streamingDrmSessionManager = new StreamingDrmSessionManager( + DemoUtil.WIDEVINE_UUID, player.getPlaybackLooper(), drmCallback, player.getMainHandler(), + player); + return Pair.create((DrmSessionManager) streamingDrmSessionManager, + getWidevineSecurityLevel(streamingDrmSessionManager) == SECURITY_LEVEL_1); + } + + private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) { + String securityLevelProperty = sessionManager.getPropertyString("securityLevel"); + return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty + .equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN; + } + + } + +} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java new file mode 100644 index 00000000000..498e087d12d --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java @@ -0,0 +1,121 @@ +/* + * 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; + +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.MediaCodecTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.Format; + +import android.widget.TextView; + +/** + * A {@link TrackRenderer} that periodically updates debugging information displayed by a + * {@link TextView}. + */ +/* package */ class DebugTrackRenderer extends TrackRenderer implements Runnable { + + private final TextView textView; + private final MediaCodecTrackRenderer renderer; + private final ChunkSampleSource videoSampleSource; + + private volatile boolean pendingFailure; + private volatile long currentPositionUs; + + public DebugTrackRenderer(TextView textView, MediaCodecTrackRenderer renderer) { + this(textView, renderer, null); + } + + public DebugTrackRenderer(TextView textView, MediaCodecTrackRenderer renderer, + ChunkSampleSource videoSampleSource) { + this.textView = textView; + this.renderer = renderer; + this.videoSampleSource = videoSampleSource; + } + + public void injectFailure() { + pendingFailure = true; + } + + @Override + protected boolean isEnded() { + return true; + } + + @Override + protected boolean isReady() { + return true; + } + + @Override + protected int doPrepare() throws ExoPlaybackException { + maybeFail(); + return STATE_PREPARED; + } + + @Override + protected void doSomeWork(long timeUs) throws ExoPlaybackException { + maybeFail(); + if (timeUs < currentPositionUs || timeUs > currentPositionUs + 1000000) { + currentPositionUs = timeUs; + textView.post(this); + } + } + + @Override + public void run() { + textView.setText(getRenderString()); + } + + private String getRenderString() { + return "ms(" + (currentPositionUs / 1000) + "), " + getQualityString() + + ", " + renderer.codecCounters.getDebugString(); + } + + private String getQualityString() { + Format format = videoSampleSource == null ? null : videoSampleSource.getFormat(); + return format == null ? "null" : "height(" + format.height + "), itag(" + format.id + ")"; + } + + @Override + protected long getCurrentPositionUs() { + return currentPositionUs; + } + + @Override + protected long getDurationUs() { + return TrackRenderer.MATCH_LONGEST; + } + + @Override + protected long getBufferedPositionUs() { + return TrackRenderer.END_OF_TRACK; + } + + @Override + protected void seekTo(long timeUs) { + currentPositionUs = timeUs; + } + + private void maybeFail() throws ExoPlaybackException { + if (pendingFailure) { + pendingFailure = false; + throw new ExoPlaybackException("fail() was called on DebugTrackRenderer"); + } + } + +} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java new file mode 100644 index 00000000000..1afca8f54f6 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java @@ -0,0 +1,69 @@ +/* + * 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; + +import com.google.android.exoplayer.FrameworkSampleSource; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; +import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback; + +import android.content.Context; +import android.media.MediaCodec; +import android.net.Uri; +import android.widget.TextView; + +/** + * A {@link RendererBuilder} for streams that can be read using + * {@link android.media.MediaExtractor}. + */ +public class DefaultRendererBuilder implements RendererBuilder { + + private final Context context; + private final Uri uri; + private final TextView debugTextView; + + public DefaultRendererBuilder(Context context, Uri uri, TextView debugTextView) { + this.context = context; + this.uri = uri; + this.debugTextView = debugTextView; + } + + @Override + public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { + // Build the video and audio renderers. + FrameworkSampleSource sampleSource = new FrameworkSampleSource(context, uri, null, 2); + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, + null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, + player.getMainHandler(), player, 50); + MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, + null, true, player.getMainHandler(), player); + + // Build the debug renderer. + TrackRenderer debugRenderer = debugTextView != null + ? new DebugTrackRenderer(debugTextView, videoRenderer) + : null; + + // Invoke the callback. + TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; + renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; + renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; + renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; + callback.onRenderers(null, null, renderers); + } + +} 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 new file mode 100644 index 00000000000..79934c712b3 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -0,0 +1,577 @@ +/* + * 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; + +import com.google.android.exoplayer.DummyTrackRenderer; +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer.AudioTrackInitializationException; +import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.MultiTrackChunkSource; +import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.text.TextTrackRenderer; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.util.PlayerControl; + +import android.media.MediaCodec.CryptoException; +import android.os.Handler; +import android.os.Looper; +import android.view.Surface; + +import java.io.IOException; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared + * with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH, + * SmoothStreaming and so on). + */ +public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, + DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, + MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer, + StreamingDrmSessionManager.EventListener { + + /** + * Builds renderers for the player. + */ + public interface RendererBuilder { + /** + * Constructs the necessary components for playback. + * + * @param player The parent player. + * @param callback The callback to invoke with the constructed components. + */ + void buildRenderers(DemoPlayer player, RendererBuilderCallback callback); + } + + /** + * A callback invoked by a {@link RendererBuilder}. + */ + public interface RendererBuilderCallback { + /** + * Invoked with the results from a {@link RendererBuilder}. + * + * @param trackNames The names of the available tracks, indexed by {@link DemoPlayer} TYPE_* + * constants. May be null if the track names are unknown. An individual element may be null + * if the track names are unknown for the corresponding type. + * @param multiTrackSources Sources capable of switching between multiple available tracks, + * indexed by {@link DemoPlayer} TYPE_* constants. May be null if there are no types with + * multiple tracks. An individual element may be null if it does not have multiple tracks. + * @param renderers Renderers indexed by {@link DemoPlayer} TYPE_* constants. An individual + * element may be null if there do not exist tracks of the corresponding type. + */ + void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources, + TrackRenderer[] renderers); + /** + * Invoked if a {@link RendererBuilder} encounters an error. + * + * @param e Describes the error. + */ + void onRenderersError(Exception e); + } + + /** + * A listener for core events. + */ + public interface Listener { + void onStateChanged(boolean playWhenReady, int playbackState); + void onError(Exception e); + void onVideoSizeChanged(int width, int height); + } + + /** + * A listener for internal errors. + *

+ * These errors are not visible to the user, and hence this listener is provided for + * informational purposes only. Note however that an internal error may cause a fatal + * error if the player fails to recover. If this happens, {@link Listener#onError(Exception)} + * will be invoked. + */ + public interface InternalErrorListener { + void onRendererInitializationError(Exception e); + void onAudioTrackInitializationError(AudioTrackInitializationException e); + void onDecoderInitializationError(DecoderInitializationException e); + void onCryptoError(CryptoException e); + void onUpstreamError(int sourceId, IOException e); + void onConsumptionError(int sourceId, IOException e); + void onDrmSessionManagerError(Exception e); + } + + /** + * A listener for debugging information. + */ + public interface InfoListener { + void onVideoFormatEnabled(int formatId, int trigger, int mediaTimeMs); + void onAudioFormatEnabled(int formatId, int trigger, int mediaTimeMs); + void onDroppedFrames(int count, long elapsed); + void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate); + void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, + int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes); + void onLoadCompleted(int sourceId); + } + + /** + * A listener for receiving notifications of timed text. + */ + public interface TextListener { + public abstract void onText(String text); + } + + // Constants pulled into this class for convenience. + public static final int STATE_IDLE = ExoPlayer.STATE_IDLE; + public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING; + public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING; + public static final int STATE_READY = ExoPlayer.STATE_READY; + public static final int STATE_ENDED = ExoPlayer.STATE_ENDED; + + public static final int DISABLED_TRACK = -1; + public static final int PRIMARY_TRACK = 0; + + public static final int RENDERER_COUNT = 4; + public static final int TYPE_VIDEO = 0; + public static final int TYPE_AUDIO = 1; + public static final int TYPE_TEXT = 2; + public static final int TYPE_DEBUG = 3; + + private static final int RENDERER_BUILDING_STATE_IDLE = 1; + private static final int RENDERER_BUILDING_STATE_BUILDING = 2; + private static final int RENDERER_BUILDING_STATE_BUILT = 3; + + private final RendererBuilder rendererBuilder; + private final ExoPlayer player; + private final PlayerControl playerControl; + private final Handler mainHandler; + private final CopyOnWriteArrayList listeners; + + private int rendererBuildingState; + private int lastReportedPlaybackState; + private boolean lastReportedPlayWhenReady; + + private Surface surface; + private InternalRendererBuilderCallback builderCallback; + private TrackRenderer videoRenderer; + + private MultiTrackChunkSource[] multiTrackSources; + private String[][] trackNames; + private int[] selectedTracks; + + private TextListener textListener; + private InternalErrorListener internalErrorListener; + private InfoListener infoListener; + + public DemoPlayer(RendererBuilder rendererBuilder) { + this.rendererBuilder = rendererBuilder; + player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000); + player.addListener(this); + playerControl = new PlayerControl(player); + mainHandler = new Handler(); + listeners = new CopyOnWriteArrayList(); + lastReportedPlaybackState = STATE_IDLE; + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + selectedTracks = new int[RENDERER_COUNT]; + // Disable text initially. + selectedTracks[TYPE_TEXT] = DISABLED_TRACK; + } + + public PlayerControl getPlayerControl() { + return playerControl; + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public void setInternalErrorListener(InternalErrorListener listener) { + internalErrorListener = listener; + } + + public void setInfoListener(InfoListener listener) { + infoListener = listener; + } + + public void setTextListener(TextListener listener) { + textListener = listener; + } + + public void setSurface(Surface surface) { + this.surface = surface; + pushSurfaceAndVideoTrack(false); + } + + public Surface getSurface() { + return surface; + } + + public void blockingClearSurface() { + surface = null; + pushSurfaceAndVideoTrack(true); + } + + public String[] getTracks(int type) { + return trackNames == null ? null : trackNames[type]; + } + + public int getSelectedTrackIndex(int type) { + return selectedTracks[type]; + } + + public void selectTrack(int type, int index) { + if (selectedTracks[type] == index) { + return; + } + selectedTracks[type] = index; + if (type == TYPE_VIDEO) { + pushSurfaceAndVideoTrack(false); + } else { + pushTrackSelection(type, true); + } + } + + public void prepare() { + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { + player.stop(); + } + if (builderCallback != null) { + builderCallback.cancel(); + } + rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; + maybeReportPlayerState(); + builderCallback = new InternalRendererBuilderCallback(); + rendererBuilder.buildRenderers(this, builderCallback); + } + + /* package */ void onRenderers(String[][] trackNames, + MultiTrackChunkSource[] multiTrackSources, TrackRenderer[] renderers) { + builderCallback = null; + // Normalize the results. + if (trackNames == null) { + trackNames = new String[RENDERER_COUNT][]; + } + if (multiTrackSources == null) { + multiTrackSources = new MultiTrackChunkSource[RENDERER_COUNT]; + } + for (int i = 0; i < RENDERER_COUNT; i++) { + if (renderers[i] == null) { + // Convert a null renderer to a dummy renderer. + renderers[i] = new DummyTrackRenderer(); + } else if (trackNames[i] == null) { + // We have a renderer so we must have at least one track, but the names are unknown. + // Initialize the correct number of null track names. + int trackCount = multiTrackSources[i] == null ? 1 : multiTrackSources[i].getTrackCount(); + trackNames[i] = new String[trackCount]; + } + } + // Complete preparation. + this.videoRenderer = renderers[TYPE_VIDEO]; + this.trackNames = trackNames; + this.multiTrackSources = multiTrackSources; + rendererBuildingState = RENDERER_BUILDING_STATE_BUILT; + maybeReportPlayerState(); + pushSurfaceAndVideoTrack(false); + pushTrackSelection(TYPE_AUDIO, true); + pushTrackSelection(TYPE_TEXT, true); + player.prepare(renderers); + } + + /* package */ void onRenderersError(Exception e) { + builderCallback = null; + if (internalErrorListener != null) { + internalErrorListener.onRendererInitializationError(e); + } + for (Listener listener : listeners) { + listener.onError(e); + } + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + maybeReportPlayerState(); + } + + public void setPlayWhenReady(boolean playWhenReady) { + player.setPlayWhenReady(playWhenReady); + } + + public void seekTo(int positionMs) { + player.seekTo(positionMs); + } + + public void release() { + if (builderCallback != null) { + builderCallback.cancel(); + builderCallback = null; + } + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + surface = null; + player.release(); + } + + + public int getPlaybackState() { + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { + return ExoPlayer.STATE_PREPARING; + } + int playerState = player.getPlaybackState(); + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT + && rendererBuildingState == RENDERER_BUILDING_STATE_IDLE) { + // This is an edge case where the renderers are built, but are still being passed to the + // player's playback thread. + return ExoPlayer.STATE_PREPARING; + } + return playerState; + } + + public int getCurrentPosition() { + return player.getCurrentPosition(); + } + + public int getDuration() { + return player.getDuration(); + } + + public int getBufferedPercentage() { + return player.getBufferedPercentage(); + } + + public boolean getPlayWhenReady() { + return player.getPlayWhenReady(); + } + + /* package */ Looper getPlaybackLooper() { + return player.getPlaybackLooper(); + } + + /* package */ Handler getMainHandler() { + return mainHandler; + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int state) { + maybeReportPlayerState(); + } + + @Override + public void onPlayerError(ExoPlaybackException exception) { + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + for (Listener listener : listeners) { + listener.onError(exception); + } + } + + @Override + public void onVideoSizeChanged(int width, int height) { + for (Listener listener : listeners) { + listener.onVideoSizeChanged(width, height); + } + } + + @Override + public void onDroppedFrames(int count, long elapsed) { + if (infoListener != null) { + infoListener.onDroppedFrames(count, elapsed); + } + } + + @Override + public void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate) { + if (infoListener != null) { + infoListener.onBandwidthSample(elapsedMs, bytes, bandwidthEstimate); + } + } + + @Override + public void onDownstreamFormatChanged(int sourceId, int formatId, int trigger, int mediaTimeMs) { + if (infoListener == null) { + return; + } + if (sourceId == TYPE_VIDEO) { + infoListener.onVideoFormatEnabled(formatId, trigger, mediaTimeMs); + } else if (sourceId == TYPE_AUDIO) { + infoListener.onAudioFormatEnabled(formatId, trigger, mediaTimeMs); + } + } + + @Override + public void onDrmSessionManagerError(Exception e) { + if (internalErrorListener != null) { + internalErrorListener.onDrmSessionManagerError(e); + } + } + + @Override + public void onDecoderInitializationError(DecoderInitializationException e) { + if (internalErrorListener != null) { + internalErrorListener.onDecoderInitializationError(e); + } + } + + @Override + public void onAudioTrackInitializationError(AudioTrackInitializationException e) { + if (internalErrorListener != null) { + internalErrorListener.onAudioTrackInitializationError(e); + } + } + + @Override + public void onCryptoError(CryptoException e) { + if (internalErrorListener != null) { + internalErrorListener.onCryptoError(e); + } + } + + @Override + public void onUpstreamError(int sourceId, IOException e) { + if (internalErrorListener != null) { + internalErrorListener.onUpstreamError(sourceId, e); + } + } + + @Override + public void onConsumptionError(int sourceId, IOException e) { + if (internalErrorListener != null) { + internalErrorListener.onConsumptionError(sourceId, e); + } + } + + @Override + public void onText(String text) { + if (textListener != null) { + textListener.onText(text); + } + } + + @Override + public void onPlayWhenReadyCommitted() { + // Do nothing. + } + + @Override + public void onDrawnToSurface(Surface surface) { + // Do nothing. + } + + @Override + public void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, + int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) { + if (infoListener != null) { + infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs, + mediaEndTimeMs, totalBytes); + } + } + + @Override + public void onLoadCompleted(int sourceId) { + if (infoListener != null) { + infoListener.onLoadCompleted(sourceId); + } + } + + @Override + public void onLoadCanceled(int sourceId) { + // Do nothing. + } + + @Override + public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, + long totalBytes) { + // Do nothing. + } + + @Override + public void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, + long totalBytes) { + // Do nothing. + } + + private void maybeReportPlayerState() { + boolean playWhenReady = player.getPlayWhenReady(); + int playbackState = getPlaybackState(); + if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) { + for (Listener listener : listeners) { + listener.onStateChanged(playWhenReady, playbackState); + } + lastReportedPlayWhenReady = playWhenReady; + lastReportedPlaybackState = playbackState; + } + } + + private void pushSurfaceAndVideoTrack(boolean blockForSurfacePush) { + if (rendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { + return; + } + + if (blockForSurfacePush) { + player.blockingSendMessage( + videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); + } else { + player.sendMessage( + videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); + } + pushTrackSelection(TYPE_VIDEO, surface != null && surface.isValid()); + } + + private void pushTrackSelection(int type, boolean allowRendererEnable) { + if (rendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { + return; + } + + int trackIndex = selectedTracks[type]; + if (trackIndex == DISABLED_TRACK) { + player.setRendererEnabled(type, false); + } else if (multiTrackSources[type] == null) { + player.setRendererEnabled(type, allowRendererEnable); + } else { + boolean playWhenReady = player.getPlayWhenReady(); + player.setPlayWhenReady(false); + player.setRendererEnabled(type, false); + player.sendMessage(multiTrackSources[type], MultiTrackChunkSource.MSG_SELECT_TRACK, + trackIndex); + player.setRendererEnabled(type, allowRendererEnable); + player.setPlayWhenReady(playWhenReady); + } + } + + private class InternalRendererBuilderCallback implements RendererBuilderCallback { + + private boolean canceled; + + public void cancel() { + canceled = true; + } + + @Override + public void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources, + TrackRenderer[] renderers) { + if (!canceled) { + DemoPlayer.this.onRenderers(trackNames, multiTrackSources, renderers); + } + } + + @Override + public void onRenderersError(Exception e) { + if (!canceled) { + DemoPlayer.this.onRenderersError(e); + } + } + + } + +} 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 new file mode 100644 index 00000000000..ea4dccee67f --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -0,0 +1,261 @@ +/* + * 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; + +import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecUtil; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.ChunkSource; +import com.google.android.exoplayer.chunk.FormatEvaluator; +import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; +import com.google.android.exoplayer.chunk.MultiTrackChunkSource; +import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; +import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback; +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.smoothstreaming.SmoothStreamingChunkSource; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestFetcher; +import com.google.android.exoplayer.text.TextTrackRenderer; +import com.google.android.exoplayer.text.ttml.TtmlParser; +import com.google.android.exoplayer.upstream.BufferPool; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.UnsupportedSchemeException; +import android.os.Handler; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.UUID; + +/** + * A {@link RendererBuilder} for SmoothStreaming. + */ +public class SmoothStreamingRendererBuilder implements RendererBuilder, + ManifestCallback { + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int VIDEO_BUFFER_SEGMENTS = 200; + private static final int AUDIO_BUFFER_SEGMENTS = 60; + private static final int TTML_BUFFER_SEGMENTS = 2; + + private final String userAgent; + private final String url; + private final String contentId; + private final MediaDrmCallback drmCallback; + private final TextView debugTextView; + + private DemoPlayer player; + private RendererBuilderCallback callback; + + public SmoothStreamingRendererBuilder(String userAgent, String url, String contentId, + MediaDrmCallback drmCallback, TextView debugTextView) { + this.userAgent = userAgent; + this.url = url; + this.contentId = contentId; + this.drmCallback = drmCallback; + this.debugTextView = debugTextView; + } + + @Override + public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { + this.player = player; + this.callback = callback; + SmoothStreamingManifestFetcher mpdFetcher = new SmoothStreamingManifestFetcher(this); + mpdFetcher.execute(url + "/Manifest", contentId); + } + + @Override + public void onManifestError(String contentId, Exception e) { + callback.onRenderersError(e); + } + + @Override + public void onManifest(String contentId, SmoothStreamingManifest manifest) { + Handler mainHandler = player.getMainHandler(); + LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); + DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); + + // Check drm support if necessary. + 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)); + return; + } + try { + drmSessionManager = V18Compat.getDrmSessionManager(manifest.protectionElement.uuid, player, + drmCallback); + } catch (UnsupportedSchemeException e) { + callback.onRenderersError(e); + return; + } + } + + // Obtain stream elements for playback. + int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); + int audioStreamElementCount = 0; + int textStreamElementCount = 0; + int videoStreamElementIndex = -1; + ArrayList videoTrackIndexList = new ArrayList(); + for (int i = 0; i < manifest.streamElements.length; i++) { + if (manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) { + audioStreamElementCount++; + } else if (manifest.streamElements[i].type == StreamElement.TYPE_TEXT) { + textStreamElementCount++; + } else if (videoStreamElementIndex == -1 + && manifest.streamElements[i].type == StreamElement.TYPE_VIDEO) { + videoStreamElementIndex = i; + StreamElement streamElement = manifest.streamElements[i]; + for (int j = 0; j < streamElement.tracks.length; j++) { + TrackElement trackElement = streamElement.tracks[j]; + if (trackElement.maxWidth * trackElement.maxHeight <= maxDecodableFrameSize) { + videoTrackIndexList.add(j); + } else { + // The device isn't capable of playing this stream. + } + } + } + } + int[] videoTrackIndices = new int[videoTrackIndexList.size()]; + for (int i = 0; i < videoTrackIndexList.size(); i++) { + videoTrackIndices[i] = videoTrackIndexList.get(i); + } + + // Build the video renderer. + DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, + bandwidthMeter); + ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest, + videoStreamElementIndex, videoTrackIndices, videoDataSource, + new AdaptiveEvaluator(bandwidthMeter)); + 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, + mainHandler, player, 50); + + // Build the audio renderer. + final String[] audioTrackNames; + final MultiTrackChunkSource audioChunkSource; + final MediaCodecAudioTrackRenderer audioRenderer; + if (audioStreamElementCount == 0) { + audioTrackNames = null; + audioChunkSource = null; + audioRenderer = null; + } else { + audioTrackNames = new String[audioStreamElementCount]; + ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount]; + DataSource audioDataSource = new HttpDataSource(userAgent, + HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter); + FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator(); + audioStreamElementCount = 0; + for (int i = 0; i < manifest.streamElements.length; i++) { + if (manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) { + audioTrackNames[audioStreamElementCount] = manifest.streamElements[i].name; + audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(url, manifest, + i, new int[] {0}, audioDataSource, audioFormatEvaluator); + audioStreamElementCount++; + } + } + audioChunkSource = new MultiTrackChunkSource(audioChunkSources); + ChunkSampleSource 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); + } + + // Build the text renderer. + final String[] textTrackNames; + final MultiTrackChunkSource textChunkSource; + final TrackRenderer textRenderer; + if (textStreamElementCount == 0) { + textTrackNames = null; + textChunkSource = null; + textRenderer = null; + } else { + textTrackNames = new String[textStreamElementCount]; + ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount]; + DataSource ttmlDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, + bandwidthMeter); + FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator(); + textStreamElementCount = 0; + for (int i = 0; i < manifest.streamElements.length; i++) { + if (manifest.streamElements[i].type == StreamElement.TYPE_TEXT) { + textTrackNames[textStreamElementCount] = manifest.streamElements[i].language; + textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(url, manifest, + i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator); + textStreamElementCount++; + } + } + textChunkSource = new MultiTrackChunkSource(textChunkSources); + ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl, + TTML_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, + DemoPlayer.TYPE_TEXT); + textRenderer = new TextTrackRenderer(ttmlSampleSource, new TtmlParser(), player, + 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; + trackNames[DemoPlayer.TYPE_TEXT] = textTrackNames; + + MultiTrackChunkSource[] multiTrackChunkSources = + new MultiTrackChunkSource[DemoPlayer.RENDERER_COUNT]; + multiTrackChunkSources[DemoPlayer.TYPE_AUDIO] = audioChunkSource; + multiTrackChunkSources[DemoPlayer.TYPE_TEXT] = textChunkSource; + + TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; + renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; + renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; + renderers[DemoPlayer.TYPE_TEXT] = textRenderer; + renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; + callback.onRenderers(trackNames, multiTrackChunkSources, renderers); + } + + @TargetApi(18) + private static class V18Compat { + + public static DrmSessionManager getDrmSessionManager(UUID uuid, DemoPlayer player, + MediaDrmCallback drmCallback) throws UnsupportedSchemeException { + return new StreamingDrmSessionManager(uuid, player.getPlaybackLooper(), drmCallback, + player.getMainHandler(), player); + } + + } + +} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java new file mode 100644 index 00000000000..ec5bde031f8 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java @@ -0,0 +1,139 @@ +/* + * 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.simple; + +import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecUtil; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.ChunkSource; +import com.google.android.exoplayer.chunk.Format; +import com.google.android.exoplayer.chunk.FormatEvaluator; +import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; +import com.google.android.exoplayer.dash.DashMp4ChunkSource; +import com.google.android.exoplayer.dash.mpd.AdaptationSet; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher; +import com.google.android.exoplayer.dash.mpd.Period; +import com.google.android.exoplayer.dash.mpd.Representation; +import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilder; +import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilderCallback; +import com.google.android.exoplayer.upstream.BufferPool; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; + +import android.media.MediaCodec; +import android.os.AsyncTask; +import android.os.Handler; + +import java.util.ArrayList; + +/** + * A {@link RendererBuilder} for DASH VOD. + */ +/* package */ class DashVodRendererBuilder implements RendererBuilder, + ManifestCallback { + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int VIDEO_BUFFER_SEGMENTS = 200; + private static final int AUDIO_BUFFER_SEGMENTS = 60; + + private final SimplePlayerActivity playerActivity; + private final String userAgent; + private final String url; + private final String contentId; + + private RendererBuilderCallback callback; + + public DashVodRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url, + String contentId) { + this.playerActivity = playerActivity; + this.userAgent = userAgent; + this.url = url; + this.contentId = contentId; + } + + @Override + public void buildRenderers(RendererBuilderCallback callback) { + this.callback = callback; + MediaPresentationDescriptionFetcher mpdFetcher = new MediaPresentationDescriptionFetcher(this); + mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId); + } + + @Override + public void onManifestError(String contentId, Exception e) { + callback.onRenderersError(e); + } + + @Override + public void onManifest(String contentId, MediaPresentationDescription manifest) { + Handler mainHandler = playerActivity.getMainHandler(); + LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); + DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + + // Obtain Representations for playback. + int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); + Representation audioRepresentation = null; + ArrayList videoRepresentationsList = new ArrayList(); + Period period = manifest.periods.get(0); + for (int i = 0; i < period.adaptationSets.size(); i++) { + AdaptationSet adaptationSet = period.adaptationSets.get(i); + int adaptationSetType = adaptationSet.type; + for (int j = 0; j < adaptationSet.representations.size(); j++) { + Representation representation = adaptationSet.representations.get(j); + if (audioRepresentation == null && adaptationSetType == AdaptationSet.TYPE_AUDIO) { + audioRepresentation = representation; + } else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { + Format format = representation.format; + if (format.width * format.height <= maxDecodableFrameSize) { + videoRepresentationsList.add(representation); + } else { + // The device isn't capable of playing this stream. + } + } + } + } + Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()]; + videoRepresentationsList.toArray(videoRepresentations); + + // Build the video renderer. + DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, + bandwidthMeter); + ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource, + new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); + ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, + VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); + + // Build the audio renderer. + DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, + bandwidthMeter); + ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource, + new FormatEvaluator.FixedEvaluator(), audioRepresentation); + SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, + AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); + MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer( + audioSampleSource); + callback.onRenderers(videoRenderer, audioRenderer); + } + +} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DefaultRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DefaultRendererBuilder.java new file mode 100644 index 00000000000..aef46d38d96 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DefaultRendererBuilder.java @@ -0,0 +1,54 @@ +/* + * 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.simple; + +import com.google.android.exoplayer.FrameworkSampleSource; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilder; +import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilderCallback; + +import android.media.MediaCodec; +import android.net.Uri; + +/** + * A {@link RendererBuilder} for streams that can be read using + * {@link android.media.MediaExtractor}. + */ +/* package */ class DefaultRendererBuilder implements RendererBuilder { + + private final SimplePlayerActivity playerActivity; + private final Uri uri; + + public DefaultRendererBuilder(SimplePlayerActivity playerActivity, Uri uri) { + this.playerActivity = playerActivity; + this.uri = uri; + } + + @Override + public void buildRenderers(RendererBuilderCallback callback) { + // Build the video and audio renderers. + FrameworkSampleSource sampleSource = new FrameworkSampleSource(playerActivity, uri, null, 2); + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, playerActivity.getMainHandler(), + playerActivity, 50); + MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource); + + // Invoke the callback. + callback.onRenderers(videoRenderer, audioRenderer); + } + +} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java new file mode 100644 index 00000000000..fa66b5c5523 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java @@ -0,0 +1,290 @@ +/* + * 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.simple; + +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.VideoSurfaceView; +import com.google.android.exoplayer.demo.DemoUtil; +import com.google.android.exoplayer.demo.R; +import com.google.android.exoplayer.util.PlayerControl; + +import android.app.Activity; +import android.content.Intent; +import android.media.MediaCodec.CryptoException; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.MediaController; +import android.widget.Toast; + +/** + * An activity that plays media using {@link ExoPlayer}. + */ +public class SimplePlayerActivity extends Activity implements SurfaceHolder.Callback, + ExoPlayer.Listener, MediaCodecVideoTrackRenderer.EventListener { + + /** + * Builds renderers for the player. + */ + public interface RendererBuilder { + + void buildRenderers(RendererBuilderCallback callback); + + } + + public static final int RENDERER_COUNT = 2; + public static final int TYPE_VIDEO = 0; + public static final int TYPE_AUDIO = 1; + + private static final String TAG = "PlayerActivity"; + + public static final int TYPE_DASH_VOD = 0; + public static final int TYPE_SS_VOD = 1; + public static final int TYPE_OTHER = 2; + + private MediaController mediaController; + private Handler mainHandler; + private View shutterView; + private VideoSurfaceView surfaceView; + + private ExoPlayer player; + private RendererBuilder builder; + private RendererBuilderCallback callback; + private MediaCodecVideoTrackRenderer videoRenderer; + + private boolean autoPlay = true; + private int playerPosition; + + private Uri contentUri; + private int contentType; + private String contentId; + + // Activity lifecycle + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + contentUri = intent.getData(); + contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, TYPE_OTHER); + contentId = intent.getStringExtra(DemoUtil.CONTENT_ID_EXTRA); + + mainHandler = new Handler(getMainLooper()); + builder = getRendererBuilder(); + + setContentView(R.layout.player_activity_simple); + View root = findViewById(R.id.root); + root.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View arg0, MotionEvent arg1) { + if (arg1.getAction() == MotionEvent.ACTION_DOWN) { + toggleControlsVisibility(); + } + return true; + } + }); + + mediaController = new MediaController(this); + mediaController.setAnchorView(root); + shutterView = findViewById(R.id.shutter); + surfaceView = (VideoSurfaceView) findViewById(R.id.surface_view); + surfaceView.getHolder().addCallback(this); + } + + @Override + public void onResume() { + super.onResume(); + // Setup the player + player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000); + player.addListener(this); + player.seekTo(playerPosition); + // Build the player controls + mediaController.setMediaPlayer(new PlayerControl(player)); + mediaController.setEnabled(true); + // Request the renderers + callback = new RendererBuilderCallback(); + builder.buildRenderers(callback); + } + + @Override + public void onPause() { + super.onPause(); + // Release the player + if (player != null) { + playerPosition = player.getCurrentPosition(); + player.release(); + player = null; + } + callback = null; + videoRenderer = null; + shutterView.setVisibility(View.VISIBLE); + } + + // Public methods + + public Handler getMainHandler() { + return mainHandler; + } + + // Internal methods + + private void toggleControlsVisibility() { + if (mediaController.isShowing()) { + mediaController.hide(); + } else { + mediaController.show(0); + } + } + + private RendererBuilder getRendererBuilder() { + String userAgent = DemoUtil.getUserAgent(this); + switch (contentType) { + case TYPE_SS_VOD: + return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(), + contentId); + case TYPE_DASH_VOD: + return new DashVodRendererBuilder(this, userAgent, contentUri.toString(), contentId); + default: + return new DefaultRendererBuilder(this, contentUri); + } + } + + private void onRenderers(RendererBuilderCallback callback, + MediaCodecVideoTrackRenderer videoRenderer, MediaCodecAudioTrackRenderer audioRenderer) { + if (this.callback != callback) { + return; + } + this.callback = null; + this.videoRenderer = videoRenderer; + player.prepare(videoRenderer, audioRenderer); + maybeStartPlayback(); + } + + private void maybeStartPlayback() { + Surface surface = surfaceView.getHolder().getSurface(); + if (videoRenderer == null || surface == null || !surface.isValid()) { + // We're not ready yet. + return; + } + player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); + if (autoPlay) { + player.setPlayWhenReady(true); + autoPlay = false; + } + } + + private void onRenderersError(RendererBuilderCallback callback, Exception e) { + if (this.callback != callback) { + return; + } + this.callback = null; + onError(e); + } + + private void onError(Exception e) { + Log.e(TAG, "Playback failed", e); + Toast.makeText(this, R.string.failed, Toast.LENGTH_SHORT).show(); + finish(); + } + + // ExoPlayer.Listener implementation + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + // Do nothing. + } + + @Override + public void onPlayWhenReadyCommitted() { + // Do nothing. + } + + @Override + public void onPlayerError(ExoPlaybackException e) { + onError(e); + } + + // MediaCodecVideoTrackRenderer.Listener + + @Override + public void onVideoSizeChanged(int width, int height) { + surfaceView.setVideoWidthHeightRatio(height == 0 ? 1 : (float) width / height); + } + + @Override + public void onDrawnToSurface(Surface surface) { + shutterView.setVisibility(View.GONE); + } + + @Override + public void onDroppedFrames(int count, long elapsed) { + Log.d(TAG, "Dropped frames: " + count); + } + + @Override + public void onDecoderInitializationError(DecoderInitializationException e) { + // This is for informational purposes only. Do nothing. + } + + @Override + public void onCryptoError(CryptoException e) { + // This is for informational purposes only. Do nothing. + } + + // SurfaceHolder.Callback implementation + + @Override + public void surfaceCreated(SurfaceHolder holder) { + maybeStartPlayback(); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + // Do nothing. + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (videoRenderer != null) { + player.blockingSendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, null); + } + } + + /* package */ final class RendererBuilderCallback { + + public void onRenderers(MediaCodecVideoTrackRenderer videoRenderer, + MediaCodecAudioTrackRenderer audioRenderer) { + SimplePlayerActivity.this.onRenderers(this, videoRenderer, audioRenderer); + } + + public void onRenderersError(Exception e) { + SimplePlayerActivity.this.onRenderersError(this, e); + } + + } + +} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java new file mode 100644 index 00000000000..80e4c105de1 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java @@ -0,0 +1,141 @@ +/* + * 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.simple; + +import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecUtil; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.ChunkSource; +import com.google.android.exoplayer.chunk.FormatEvaluator; +import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; +import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilder; +import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilderCallback; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestFetcher; +import com.google.android.exoplayer.upstream.BufferPool; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; + +import android.media.MediaCodec; +import android.os.Handler; + +import java.util.ArrayList; + +/** + * A {@link RendererBuilder} for SmoothStreaming. + */ +/* package */ class SmoothStreamingRendererBuilder implements RendererBuilder, + ManifestCallback { + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int VIDEO_BUFFER_SEGMENTS = 200; + private static final int AUDIO_BUFFER_SEGMENTS = 60; + + private final SimplePlayerActivity playerActivity; + private final String userAgent; + private final String url; + private final String contentId; + + private RendererBuilderCallback callback; + + public SmoothStreamingRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, + String url, String contentId) { + this.playerActivity = playerActivity; + this.userAgent = userAgent; + this.url = url; + this.contentId = contentId; + } + + @Override + public void buildRenderers(RendererBuilderCallback callback) { + this.callback = callback; + SmoothStreamingManifestFetcher mpdFetcher = new SmoothStreamingManifestFetcher(this); + mpdFetcher.execute(url + "/Manifest", contentId); + } + + @Override + public void onManifestError(String contentId, Exception e) { + callback.onRenderersError(e); + } + + @Override + public void onManifest(String contentId, SmoothStreamingManifest manifest) { + Handler mainHandler = playerActivity.getMainHandler(); + LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); + DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + + // Obtain stream elements for playback. + int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); + int audioStreamElementIndex = -1; + int videoStreamElementIndex = -1; + ArrayList videoTrackIndexList = new ArrayList(); + for (int i = 0; i < manifest.streamElements.length; i++) { + if (audioStreamElementIndex == -1 + && manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) { + audioStreamElementIndex = i; + } else if (videoStreamElementIndex == -1 + && manifest.streamElements[i].type == StreamElement.TYPE_VIDEO) { + videoStreamElementIndex = i; + StreamElement streamElement = manifest.streamElements[i]; + for (int j = 0; j < streamElement.tracks.length; j++) { + TrackElement trackElement = streamElement.tracks[j]; + if (trackElement.maxWidth * trackElement.maxHeight <= maxDecodableFrameSize) { + videoTrackIndexList.add(j); + } else { + // The device isn't capable of playing this stream. + } + } + } + } + int[] videoTrackIndices = new int[videoTrackIndexList.size()]; + for (int i = 0; i < videoTrackIndexList.size(); i++) { + videoTrackIndices[i] = videoTrackIndexList.get(i); + } + + // Build the video renderer. + DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, + bandwidthMeter); + ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest, + videoStreamElementIndex, videoTrackIndices, videoDataSource, + new AdaptiveEvaluator(bandwidthMeter)); + ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, + VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); + + // Build the audio renderer. + DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, + bandwidthMeter); + ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest, + audioStreamElementIndex, new int[] {0}, audioDataSource, + new FormatEvaluator.FixedEvaluator()); + SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, + AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true); + MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer( + audioSampleSource); + callback.onRenderers(videoRenderer, audioRenderer); + } + +} diff --git a/demo/src/main/project.properties b/demo/src/main/project.properties new file mode 100644 index 00000000000..d194d6402ef --- /dev/null +++ b/demo/src/main/project.properties @@ -0,0 +1,13 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "ant.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-19 +android.library=false +android.library.reference.1=../../../library/src/main diff --git a/demo/src/main/res/layout/player_activity_full.xml b/demo/src/main/res/layout/player_activity_full.xml new file mode 100644 index 00000000000..8d3e132995f --- /dev/null +++ b/demo/src/main/res/layout/player_activity_full.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + +