Initial drop. 1.0.10.
This commit is contained in:
commit
27ab5c83a6
30
CONTRIBUTING.md
Normal file
30
CONTRIBUTING.md
Normal file
@ -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
|
202
LICENSE
Normal file
202
LICENSE
Normal file
@ -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.
|
43
README.md
Normal file
43
README.md
Normal file
@ -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
|
30
build.gradle
Normal file
30
build.gradle
Normal file
@ -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()
|
||||
}
|
||||
}
|
38
demo/build.gradle
Normal file
38
demo/build.gradle
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright (C) 2014 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
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')
|
||||
}
|
10
demo/src/main/.classpath
Normal file
10
demo/src/main/.classpath
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
|
||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
|
||||
<classpathentry combineaccessrules="false" kind="src" path="/ExoPlayerLib"/>
|
||||
<classpathentry kind="src" path="gen"/>
|
||||
<classpathentry kind="src" path="java"/>
|
||||
<classpathentry kind="output" path="bin/classes"/>
|
||||
</classpath>
|
53
demo/src/main/.project
Normal file
53
demo/src/main/.project
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>ExoPlayerDemo</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1363908154650</id>
|
||||
<name></name>
|
||||
<type>22</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.ui.ide.multiFilter</id>
|
||||
<arguments>1.0-name-matches-false-false-BUILD</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
<filter>
|
||||
<id>1363908154652</id>
|
||||
<name></name>
|
||||
<type>10</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.ui.ide.multiFilter</id>
|
||||
<arguments>1.0-name-matches-true-false-build</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
4
demo/src/main/.settings/org.eclipse.jdt.core.prefs
Normal file
4
demo/src/main/.settings/org.eclipse.jdt.core.prefs
Normal file
@ -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
|
55
demo/src/main/AndroidManifest.xml
Normal file
55
demo/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer.demo"
|
||||
android:versionCode="1010"
|
||||
android:versionName="1.0.10"
|
||||
android:theme="@style/RootTheme">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="19"/>
|
||||
|
||||
<application
|
||||
android:label="@string/application_name"
|
||||
android:allowBackup="true">
|
||||
|
||||
<activity android:name="com.google.android.exoplayer.demo.SampleChooserActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:label="@string/application_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="com.google.android.exoplayer.demo.simple.SimplePlayerActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/PlayerTheme"/>
|
||||
|
||||
<activity android:name="com.google.android.exoplayer.demo.full.FullPlayerActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/PlayerTheme"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -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<String, String> 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<String, String> 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;
|
||||
}
|
||||
|
||||
}
|
@ -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<Object> {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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() {}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<String, String> KEY_REQUEST_PROPERTIES;
|
||||
static {
|
||||
HashMap<String, String> keyRequestProperties = new HashMap<String, String>();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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<MediaPresentationDescription> {
|
||||
|
||||
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<Representation> audioRepresentationsList = new ArrayList<Representation>();
|
||||
ArrayList<Representation> videoRepresentationsList = new ArrayList<Representation>();
|
||||
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<DrmSessionManager, Boolean> 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<Representation> sdRepresentations = new ArrayList<Representation>();
|
||||
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<DrmSessionManager, Boolean> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
* <p>
|
||||
* 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<Listener> 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<Listener>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<SmoothStreamingManifest> {
|
||||
|
||||
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<Integer> videoTrackIndexList = new ArrayList<Integer>();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<MediaPresentationDescription> {
|
||||
|
||||
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<Representation> videoRepresentationsList = new ArrayList<Representation>();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<SmoothStreamingManifest> {
|
||||
|
||||
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<Integer> videoTrackIndexList = new ArrayList<Integer>();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
13
demo/src/main/project.properties
Normal file
13
demo/src/main/project.properties
Normal file
@ -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
|
111
demo/src/main/res/layout/player_activity_full.xml
Normal file
111
demo/src/main/res/layout/player_activity_full.xml
Normal file
@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer.VideoSurfaceView android:id="@+id/surface_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
<TextView android:id="@+id/subtitles"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center|bottom"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingBottom="32dp"
|
||||
android:gravity="center"
|
||||
android:textSize="20sp"
|
||||
android:visibility="invisible"/>
|
||||
|
||||
<View android:id="@+id/shutter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#88000000"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView android:id="@+id/player_state_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:textSize="10sp"/>
|
||||
|
||||
<TextView android:id="@+id/debug_text_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:textSize="10sp"/>
|
||||
|
||||
<LinearLayout android:id="@+id/controls_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<Button android:id="@+id/video_controls"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/video"
|
||||
style="@style/DemoButton"
|
||||
android:visibility="gone"
|
||||
android:onClick="showVideoPopup"/>
|
||||
|
||||
<Button android:id="@+id/audio_controls"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/audio"
|
||||
style="@style/DemoButton"
|
||||
android:visibility="gone"
|
||||
android:onClick="showAudioPopup"/>
|
||||
|
||||
<Button android:id="@+id/text_controls"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/text"
|
||||
style="@style/DemoButton"
|
||||
android:visibility="gone"
|
||||
android:onClick="showTextPopup"/>
|
||||
|
||||
<Button android:id="@+id/verbose_log_controls"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/logging"
|
||||
style="@style/DemoButton"
|
||||
android:onClick="showVerboseLogPopup"/>
|
||||
|
||||
<Button android:id="@+id/retry_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/retry"
|
||||
android:visibility="gone"
|
||||
style="@style/DemoButton"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
32
demo/src/main/res/layout/player_activity_simple.xml
Normal file
32
demo/src/main/res/layout/player_activity_simple.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer.VideoSurfaceView android:id="@+id/surface_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
<View android:id="@+id/shutter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"/>
|
||||
|
||||
</FrameLayout>
|
25
demo/src/main/res/layout/sample_chooser_activity.xml
Normal file
25
demo/src/main/res/layout/sample_chooser_activity.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ListView android:id="@+id/sample_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</LinearLayout>
|
25
demo/src/main/res/layout/sample_chooser_inline_header.xml
Normal file
25
demo/src/main/res/layout/sample_chooser_inline_header.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:padding="8dp"
|
||||
android:focusable="true"
|
||||
android:background="#339999FF"/>
|
46
demo/src/main/res/values/strings.xml
Normal file
46
demo/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
|
||||
<!-- The user visible name of the application. [CHAR LIMIT=20] -->
|
||||
<string name="application_name">ExoPlayer Demo</string>
|
||||
|
||||
<string name="enable_background_audio">Play in background</string>
|
||||
|
||||
<string name="video">Video</string>
|
||||
|
||||
<string name="audio">Audio</string>
|
||||
|
||||
<string name="text">Text</string>
|
||||
|
||||
<string name="logging">Logging</string>
|
||||
|
||||
<string name="logging_normal">Normal</string>
|
||||
|
||||
<string name="logging_verbose">Verbose</string>
|
||||
|
||||
<string name="retry">Retry</string>
|
||||
|
||||
<string name="off">[off]</string>
|
||||
|
||||
<string name="on">[on]</string>
|
||||
|
||||
<string name="drm_not_supported">Protected content not supported on API levels below 18</string>
|
||||
|
||||
<string name="failed">Playback failed</string>
|
||||
|
||||
</resources>
|
33
demo/src/main/res/values/styles.xml
Normal file
33
demo/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="RootTheme" parent="android:Theme.Holo">
|
||||
</style>
|
||||
|
||||
<style name="PlayerTheme" parent="@style/RootTheme">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="DemoButton">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:minWidth">40dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
18
gradle.properties
Normal file
18
gradle.properties
Normal file
@ -0,0 +1,18 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Settings specified in this file will override any Gradle settings
|
||||
# configured through the IDE.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
#Tue Jun 10 20:02:28 BST 2014
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-bin.zip
|
164
gradlew
vendored
Executable file
164
gradlew
vendored
Executable file
@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched.
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
fi
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >&-
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >&-
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
90
gradlew.bat
vendored
Normal file
90
gradlew.bat
vendored
Normal file
@ -0,0 +1,90 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
53
library/.project~
Normal file
53
library/.project~
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>ExoPlayerLib</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1363908161147</id>
|
||||
<name></name>
|
||||
<type>22</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.ui.ide.multiFilter</id>
|
||||
<arguments>1.0-name-matches-false-false-BUILD</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
<filter>
|
||||
<id>1363908161148</id>
|
||||
<name></name>
|
||||
<type>10</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.ui.ide.multiFilter</id>
|
||||
<arguments>1.0-name-matches-true-false-build</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
38
library/build.gradle
Normal file
38
library/build.gradle
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright (C) 2014 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply plugin: 'android-library'
|
||||
|
||||
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 {
|
||||
}
|
BIN
library/doc_src/images/exoplayer_playbackstate.png
Normal file
BIN
library/doc_src/images/exoplayer_playbackstate.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
library/doc_src/images/exoplayer_state.png
Normal file
BIN
library/doc_src/images/exoplayer_state.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
library/doc_src/images/exoplayer_threading_model.png
Normal file
BIN
library/doc_src/images/exoplayer_threading_model.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
BIN
library/doc_src/images/trackrenderer_state.png
Normal file
BIN
library/doc_src/images/trackrenderer_state.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
9
library/src/main/.classpath
Normal file
9
library/src/main/.classpath
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
|
||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
|
||||
<classpathentry kind="src" path="java"/>
|
||||
<classpathentry kind="src" path="gen"/>
|
||||
<classpathentry kind="output" path="bin/classes"/>
|
||||
</classpath>
|
53
library/src/main/.project
Normal file
53
library/src/main/.project
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>ExoPlayerLib</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1363908161147</id>
|
||||
<name></name>
|
||||
<type>22</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.ui.ide.multiFilter</id>
|
||||
<arguments>1.0-name-matches-false-false-BUILD</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
<filter>
|
||||
<id>1363908161148</id>
|
||||
<name></name>
|
||||
<type>10</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.ui.ide.multiFilter</id>
|
||||
<arguments>1.0-name-matches-true-false-build</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
4
library/src/main/.settings/org.eclipse.jdt.core.prefs
Normal file
4
library/src/main/.settings/org.eclipse.jdt.core.prefs
Normal file
@ -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
|
26
library/src/main/AndroidManifest.xml
Normal file
26
library/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19"/>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Maintains codec event counts, for debugging purposes only.
|
||||
*/
|
||||
public final class CodecCounters {
|
||||
|
||||
public volatile long codecInitCount;
|
||||
public volatile long codecReleaseCount;
|
||||
public volatile long outputFormatChangedCount;
|
||||
public volatile long outputBuffersChangedCount;
|
||||
public volatile long queuedInputBufferCount;
|
||||
public volatile long inputBufferWaitingForSampleCount;
|
||||
public volatile long keyframeCount;
|
||||
public volatile long queuedEndOfStreamCount;
|
||||
public volatile long renderedOutputBufferCount;
|
||||
public volatile long skippedOutputBufferCount;
|
||||
public volatile long droppedOutputBufferCount;
|
||||
public volatile long discardedSamplesCount;
|
||||
|
||||
/**
|
||||
* Resets all counts to zero.
|
||||
*/
|
||||
public void zeroAllCounts() {
|
||||
codecInitCount = 0;
|
||||
codecReleaseCount = 0;
|
||||
outputFormatChangedCount = 0;
|
||||
outputBuffersChangedCount = 0;
|
||||
queuedInputBufferCount = 0;
|
||||
inputBufferWaitingForSampleCount = 0;
|
||||
keyframeCount = 0;
|
||||
queuedEndOfStreamCount = 0;
|
||||
renderedOutputBufferCount = 0;
|
||||
skippedOutputBufferCount = 0;
|
||||
droppedOutputBufferCount = 0;
|
||||
discardedSamplesCount = 0;
|
||||
}
|
||||
|
||||
public String getDebugString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("cic(").append(codecInitCount).append(")");
|
||||
builder.append("crc(").append(codecReleaseCount).append(")");
|
||||
builder.append("ofc(").append(outputFormatChangedCount).append(")");
|
||||
builder.append("obc(").append(outputBuffersChangedCount).append(")");
|
||||
builder.append("qib(").append(queuedInputBufferCount).append(")");
|
||||
builder.append("wib(").append(inputBufferWaitingForSampleCount).append(")");
|
||||
builder.append("kfc(").append(keyframeCount).append(")");
|
||||
builder.append("qes(").append(queuedEndOfStreamCount).append(")");
|
||||
builder.append("ren(").append(renderedOutputBufferCount).append(")");
|
||||
builder.append("sob(").append(skippedOutputBufferCount).append(")");
|
||||
builder.append("dob(").append(droppedOutputBufferCount).append(")");
|
||||
builder.append("dsc(").append(discardedSamplesCount).append(")");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaExtractor;
|
||||
|
||||
/**
|
||||
* Compatibility wrapper around {@link android.media.MediaCodec.CryptoInfo}.
|
||||
*/
|
||||
public class CryptoInfo {
|
||||
|
||||
/**
|
||||
* @see android.media.MediaCodec.CryptoInfo#iv
|
||||
*/
|
||||
public byte[] iv;
|
||||
/**
|
||||
* @see android.media.MediaCodec.CryptoInfo#key
|
||||
*/
|
||||
public byte[] key;
|
||||
/**
|
||||
* @see android.media.MediaCodec.CryptoInfo#mode
|
||||
*/
|
||||
public int mode;
|
||||
/**
|
||||
* @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData
|
||||
*/
|
||||
public int[] numBytesOfClearData;
|
||||
/**
|
||||
* @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData
|
||||
*/
|
||||
public int[] numBytesOfEncryptedData;
|
||||
/**
|
||||
* @see android.media.MediaCodec.CryptoInfo#numSubSamples
|
||||
*/
|
||||
public int numSubSamples;
|
||||
|
||||
private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo;
|
||||
|
||||
public CryptoInfo() {
|
||||
frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int)
|
||||
*/
|
||||
public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData,
|
||||
byte[] key, byte[] iv, int mode) {
|
||||
this.numSubSamples = numSubSamples;
|
||||
this.numBytesOfClearData = numBytesOfClearData;
|
||||
this.numBytesOfEncryptedData = numBytesOfEncryptedData;
|
||||
this.key = key;
|
||||
this.iv = iv;
|
||||
this.mode = mode;
|
||||
if (Util.SDK_INT >= 16) {
|
||||
updateFrameworkCryptoInfoV16();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to {@link MediaExtractor#getSampleCryptoInfo(android.media.MediaCodec.CryptoInfo)}.
|
||||
*
|
||||
* @param extractor The extractor from which to retrieve the crypto information.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public void setFromExtractorV16(MediaExtractor extractor) {
|
||||
extractor.getSampleCryptoInfo(frameworkCryptoInfo);
|
||||
numSubSamples = frameworkCryptoInfo.numSubSamples;
|
||||
numBytesOfClearData = frameworkCryptoInfo.numBytesOfClearData;
|
||||
numBytesOfEncryptedData = frameworkCryptoInfo.numBytesOfEncryptedData;
|
||||
key = frameworkCryptoInfo.key;
|
||||
iv = frameworkCryptoInfo.iv;
|
||||
mode = frameworkCryptoInfo.mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
|
||||
* <p>
|
||||
* Successive calls to this method on a single {@link CryptoInfo} will return the same instance.
|
||||
* Changes to the {@link CryptoInfo} will be reflected in the returned object. The return object
|
||||
* should not be modified directly.
|
||||
*
|
||||
* @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() {
|
||||
return frameworkCryptoInfo;
|
||||
}
|
||||
|
||||
@TargetApi(16)
|
||||
private android.media.MediaCodec.CryptoInfo newFrameworkCryptoInfoV16() {
|
||||
return new android.media.MediaCodec.CryptoInfo();
|
||||
}
|
||||
|
||||
@TargetApi(16)
|
||||
private void updateFrameworkCryptoInfoV16() {
|
||||
frameworkCryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, key, iv,
|
||||
mode);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Contains information about a media decoder.
|
||||
*/
|
||||
public final class DecoderInfo {
|
||||
|
||||
/**
|
||||
* The name of the decoder.
|
||||
* <p>
|
||||
* May be passed to {@link android.media.MediaCodec#createByCodecName(String)} to create an
|
||||
* instance of the decoder.
|
||||
*/
|
||||
public final String name;
|
||||
|
||||
/**
|
||||
* Whether the decoder is adaptive.
|
||||
*
|
||||
* @see android.media.MediaCodecInfo.CodecCapabilities#isFeatureSupported(String)
|
||||
* @see android.media.MediaCodecInfo.CodecCapabilities#FEATURE_AdaptivePlayback
|
||||
*/
|
||||
public final boolean adaptive;
|
||||
|
||||
/**
|
||||
* @param name The name of the decoder.
|
||||
* @param adaptive Whether the decoder is adaptive.
|
||||
*/
|
||||
/* package */ DecoderInfo(String name, boolean adaptive) {
|
||||
this.name = name;
|
||||
this.adaptive = adaptive;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,284 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.upstream.Allocator;
|
||||
import com.google.android.exoplayer.upstream.NetworkLock;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link LoadControl} implementation that allows loads to continue in a sequence that prevents
|
||||
* any loader from getting too far ahead or behind any of the other loaders.
|
||||
* <p>
|
||||
* Loads are scheduled so as to fill the available buffer space as rapidly as possible. Once the
|
||||
* duration of buffered media and the buffer utilization both exceed respective thresholds, the
|
||||
* control switches to a draining state during which no loads are permitted to start. During
|
||||
* draining periods, resources such as the device radio have an opportunity to switch into low
|
||||
* power modes. The control reverts back to the loading state when either the duration of buffered
|
||||
* media or the buffer utilization fall below respective thresholds.
|
||||
* <p>
|
||||
* This implementation of {@link LoadControl} integrates with {@link NetworkLock}, by registering
|
||||
* itself as a task with priority {@link NetworkLock#STREAMING_PRIORITY} during loading periods,
|
||||
* and unregistering itself during draining periods.
|
||||
*/
|
||||
public class DefaultLoadControl implements LoadControl {
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be notified of {@link DefaultLoadControl} events.
|
||||
*/
|
||||
public interface EventListener {
|
||||
|
||||
/**
|
||||
* Invoked when the control transitions from a loading to a draining state, or vice versa.
|
||||
*
|
||||
* @param loading Whether the control is now in a loading state.
|
||||
*/
|
||||
void onLoadingChanged(boolean loading);
|
||||
|
||||
}
|
||||
|
||||
public static final int DEFAULT_LOW_WATERMARK_MS = 15000;
|
||||
public static final int DEFAULT_HIGH_WATERMARK_MS = 30000;
|
||||
public static final float DEFAULT_LOW_POOL_LOAD = 0.2f;
|
||||
public static final float DEFAULT_HIGH_POOL_LOAD = 0.8f;
|
||||
|
||||
private static final int ABOVE_HIGH_WATERMARK = 0;
|
||||
private static final int BETWEEN_WATERMARKS = 1;
|
||||
private static final int BELOW_LOW_WATERMARK = 2;
|
||||
|
||||
private final Allocator allocator;
|
||||
private final List<Object> loaders;
|
||||
private final HashMap<Object, LoaderState> loaderStates;
|
||||
private final Handler eventHandler;
|
||||
private final EventListener eventListener;
|
||||
|
||||
private final long lowWatermarkUs;
|
||||
private final long highWatermarkUs;
|
||||
private final float lowPoolLoad;
|
||||
private final float highPoolLoad;
|
||||
|
||||
private int targetBufferSize;
|
||||
private long maxLoadStartPositionUs;
|
||||
private int bufferPoolState;
|
||||
private boolean fillingBuffers;
|
||||
private boolean streamingPrioritySet;
|
||||
|
||||
/**
|
||||
* Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
|
||||
*
|
||||
* @param allocator The {@link Allocator} used by the loader.
|
||||
*/
|
||||
public DefaultLoadControl(Allocator allocator) {
|
||||
this(allocator, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
|
||||
*
|
||||
* @param allocator The {@link Allocator} used by the loader.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
*/
|
||||
public DefaultLoadControl(Allocator allocator, Handler eventHandler,
|
||||
EventListener eventListener) {
|
||||
this(allocator, eventHandler, eventListener, DEFAULT_LOW_WATERMARK_MS,
|
||||
DEFAULT_HIGH_WATERMARK_MS, DEFAULT_LOW_POOL_LOAD, DEFAULT_HIGH_POOL_LOAD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param allocator The {@link Allocator} used by the loader.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param lowWatermarkMs The minimum duration of media that can be buffered for the control to
|
||||
* be in the draining state. If less media is buffered, then the control will transition to
|
||||
* the filling state.
|
||||
* @param highWatermarkMs The minimum duration of media that can be buffered for the control to
|
||||
* transition from filling to draining.
|
||||
* @param lowPoolLoad The minimum fraction of the buffer that must be utilized for the control
|
||||
* to be in the draining state. If the utilization is lower, then the control will transition
|
||||
* to the filling state.
|
||||
* @param highPoolLoad The minimum fraction of the buffer that must be utilized for the control
|
||||
* to transition from the loading state to the draining state.
|
||||
*/
|
||||
public DefaultLoadControl(Allocator allocator, Handler eventHandler, EventListener eventListener,
|
||||
int lowWatermarkMs, int highWatermarkMs, float lowPoolLoad, float highPoolLoad) {
|
||||
this.allocator = allocator;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
this.loaders = new ArrayList<Object>();
|
||||
this.loaderStates = new HashMap<Object, LoaderState>();
|
||||
this.lowWatermarkUs = lowWatermarkMs * 1000L;
|
||||
this.highWatermarkUs = highWatermarkMs * 1000L;
|
||||
this.lowPoolLoad = lowPoolLoad;
|
||||
this.highPoolLoad = highPoolLoad;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Object loader, int bufferSizeContribution) {
|
||||
loaders.add(loader);
|
||||
loaderStates.put(loader, new LoaderState(bufferSizeContribution));
|
||||
targetBufferSize += bufferSizeContribution;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister(Object loader) {
|
||||
loaders.remove(loader);
|
||||
LoaderState state = loaderStates.remove(loader);
|
||||
targetBufferSize -= state.bufferSizeContribution;
|
||||
updateControlState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trimAllocator() {
|
||||
allocator.trim(targetBufferSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Allocator getAllocator() {
|
||||
return allocator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs,
|
||||
boolean loading, boolean failed) {
|
||||
// Update the loader state.
|
||||
int loaderBufferState = getLoaderBufferState(playbackPositionUs, nextLoadPositionUs);
|
||||
LoaderState loaderState = loaderStates.get(loader);
|
||||
boolean loaderStateChanged = loaderState.bufferState != loaderBufferState ||
|
||||
loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading ||
|
||||
loaderState.failed != failed;
|
||||
if (loaderStateChanged) {
|
||||
loaderState.bufferState = loaderBufferState;
|
||||
loaderState.nextLoadPositionUs = nextLoadPositionUs;
|
||||
loaderState.loading = loading;
|
||||
loaderState.failed = failed;
|
||||
}
|
||||
|
||||
// Update the buffer pool state.
|
||||
int allocatedSize = allocator.getAllocatedSize();
|
||||
int bufferPoolState = getBufferPoolState(allocatedSize);
|
||||
boolean bufferPoolStateChanged = this.bufferPoolState != bufferPoolState;
|
||||
if (bufferPoolStateChanged) {
|
||||
this.bufferPoolState = bufferPoolState;
|
||||
}
|
||||
|
||||
// If either of the individual states have changed, update the shared control state.
|
||||
if (loaderStateChanged || bufferPoolStateChanged) {
|
||||
updateControlState();
|
||||
}
|
||||
|
||||
return allocatedSize < targetBufferSize && nextLoadPositionUs != -1
|
||||
&& nextLoadPositionUs <= maxLoadStartPositionUs;
|
||||
}
|
||||
|
||||
private int getLoaderBufferState(long playbackPositionUs, long nextLoadPositionUs) {
|
||||
if (nextLoadPositionUs == -1) {
|
||||
return ABOVE_HIGH_WATERMARK;
|
||||
} else {
|
||||
long timeUntilNextLoadPosition = nextLoadPositionUs - playbackPositionUs;
|
||||
return timeUntilNextLoadPosition > highWatermarkUs ? ABOVE_HIGH_WATERMARK :
|
||||
timeUntilNextLoadPosition < lowWatermarkUs ? BELOW_LOW_WATERMARK :
|
||||
BETWEEN_WATERMARKS;
|
||||
}
|
||||
}
|
||||
|
||||
private int getBufferPoolState(int allocatedSize) {
|
||||
float bufferPoolLoad = (float) allocatedSize / targetBufferSize;
|
||||
return bufferPoolLoad > highPoolLoad ? ABOVE_HIGH_WATERMARK :
|
||||
bufferPoolLoad < lowPoolLoad ? BELOW_LOW_WATERMARK :
|
||||
BETWEEN_WATERMARKS;
|
||||
}
|
||||
|
||||
private void updateControlState() {
|
||||
boolean loading = false;
|
||||
boolean failed = false;
|
||||
boolean finished = true;
|
||||
int highestState = bufferPoolState;
|
||||
for (int i = 0; i < loaders.size(); i++) {
|
||||
LoaderState loaderState = loaderStates.get(loaders.get(i));
|
||||
loading |= loaderState.loading;
|
||||
failed |= loaderState.failed;
|
||||
finished &= loaderState.nextLoadPositionUs == -1;
|
||||
highestState = Math.max(highestState, loaderState.bufferState);
|
||||
}
|
||||
|
||||
fillingBuffers = !loaders.isEmpty() && !finished && !failed
|
||||
&& (highestState == BELOW_LOW_WATERMARK
|
||||
|| (highestState == BETWEEN_WATERMARKS && fillingBuffers));
|
||||
if (fillingBuffers && !streamingPrioritySet) {
|
||||
NetworkLock.instance.add(NetworkLock.STREAMING_PRIORITY);
|
||||
streamingPrioritySet = true;
|
||||
notifyLoadingChanged(true);
|
||||
} else if (!fillingBuffers && streamingPrioritySet && !loading) {
|
||||
NetworkLock.instance.remove(NetworkLock.STREAMING_PRIORITY);
|
||||
streamingPrioritySet = false;
|
||||
notifyLoadingChanged(false);
|
||||
}
|
||||
|
||||
maxLoadStartPositionUs = -1;
|
||||
if (fillingBuffers) {
|
||||
for (int i = 0; i < loaders.size(); i++) {
|
||||
Object loader = loaders.get(i);
|
||||
LoaderState loaderState = loaderStates.get(loader);
|
||||
long loaderTime = loaderState.nextLoadPositionUs;
|
||||
if (loaderTime != -1
|
||||
&& (maxLoadStartPositionUs == -1 || loaderTime < maxLoadStartPositionUs)) {
|
||||
maxLoadStartPositionUs = loaderTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyLoadingChanged(final boolean loading) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onLoadingChanged(loading);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static class LoaderState {
|
||||
|
||||
public final int bufferSizeContribution;
|
||||
|
||||
public int bufferState;
|
||||
public boolean loading;
|
||||
public boolean failed;
|
||||
public long nextLoadPositionUs;
|
||||
|
||||
public LoaderState(int bufferSizeContribution) {
|
||||
this.bufferSizeContribution = bufferSizeContribution;
|
||||
bufferState = ABOVE_HIGH_WATERMARK;
|
||||
loading = false;
|
||||
failed = false;
|
||||
nextLoadPositionUs = -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* A {@link TrackRenderer} that does nothing.
|
||||
* <p>
|
||||
* This renderer returns {@link TrackRenderer#STATE_IGNORE} from {@link #doPrepare()} in order to
|
||||
* request that it should be ignored. {@link IllegalStateException} is thrown from all methods that
|
||||
* are documented to indicate that they should not be invoked unless the renderer is prepared.
|
||||
*/
|
||||
public class DummyTrackRenderer extends TrackRenderer {
|
||||
|
||||
@Override
|
||||
protected int doPrepare() throws ExoPlaybackException {
|
||||
return STATE_IGNORE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isEnded() {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isReady() {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seekTo(long timeUs) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSomeWork(long timeUs) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getDurationUs() {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getBufferedPositionUs() {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getCurrentPositionUs() {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Thrown when a non-recoverable playback failure occurs.
|
||||
* <p>
|
||||
* Where possible, the cause returned by {@link #getCause()} will indicate the reason for failure.
|
||||
*/
|
||||
public class ExoPlaybackException extends Exception {
|
||||
|
||||
public ExoPlaybackException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ExoPlaybackException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ExoPlaybackException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,389 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import android.os.Looper;
|
||||
|
||||
/**
|
||||
* An extensible media player exposing traditional high-level media player functionality, such as
|
||||
* the ability to prepare, play, pause and seek.
|
||||
*
|
||||
* <p>Topics covered here are:
|
||||
* <ol>
|
||||
* <li><a href="#Assumptions">Assumptions and player composition</a>
|
||||
* <li><a href="#Threading">Threading model</a>
|
||||
* <li><a href="#State">Player state</a>
|
||||
* </ol>
|
||||
*
|
||||
* <a name="Assumptions"></a>
|
||||
* <h3>Assumptions and player construction</h3>
|
||||
*
|
||||
* <p>The implementation is designed make no assumptions about (and hence impose no restrictions
|
||||
* on) the type of the media being played, how and where it is stored, or how it is rendered.
|
||||
* Rather than implementing the loading and rendering of media directly, {@link ExoPlayer} instead
|
||||
* delegates this work to one or more {@link TrackRenderer}s, which are injected when the player
|
||||
* is prepared. Hence {@link ExoPlayer} is capable of loading and playing any media for which a
|
||||
* {@link TrackRenderer} implementation can be provided.
|
||||
*
|
||||
* <p>{@link MediaCodecAudioTrackRenderer} and {@link MediaCodecVideoTrackRenderer} can be used for
|
||||
* the common cases of rendering audio and video. These components in turn require an
|
||||
* <i>upstream</i> {@link SampleSource} to be injected through their constructors, where upstream
|
||||
* is defined to denote a component that is closer to the source of the media. This pattern of
|
||||
* upstream dependency injection is actively encouraged, since it means that the functionality of
|
||||
* the player is built up through the composition of components that can easily be exchanged for
|
||||
* alternate implementations. For example a {@link SampleSource} implementation may require a
|
||||
* further upstream data loading component to be injected through its constructor, with different
|
||||
* implementations enabling the loading of data from various sources.
|
||||
*
|
||||
* <a name="Threading"></a>
|
||||
* <h3>Threading model</h3>
|
||||
*
|
||||
* <p>The figure below shows the {@link ExoPlayer} threading model.</p>
|
||||
* <p align="center"><img src="../../../../../doc_src/images/exoplayer_threading_model.png"
|
||||
* alt="MediaPlayer state diagram"
|
||||
* border="0"/></p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>It is recommended that instances are created and accessed from a single application thread.
|
||||
* An application's main thread is ideal. Accessing an instance from multiple threads is
|
||||
* discouraged, however if an application does wish to do this then it may do so provided that it
|
||||
* ensures accesses are synchronized.
|
||||
* </li>
|
||||
* <li>Registered {@link Listener}s are invoked on the thread that created the {@link ExoPlayer}
|
||||
* instance.</li>
|
||||
* <li>An internal playback thread is responsible for managing playback and invoking the
|
||||
* {@link TrackRenderer}s in order to load and play the media.</li>
|
||||
* <li>{@link TrackRenderer} implementations (or any upstream components that they depend on) may
|
||||
* use additional background threads (e.g. to load data). These are implementation specific.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <a name="State"></a>
|
||||
* <h3>Player state</h3>
|
||||
*
|
||||
* <p>The components of an {@link ExoPlayer}'s state can be divided into two distinct groups. State
|
||||
* accessed by {@link #getRendererEnabled(int)} and {@link #getPlayWhenReady()} are only ever
|
||||
* changed by invoking the player's methods, and are never changed as a result of operations that
|
||||
* have been performed asynchronously by the playback thread. In contrast, the playback state
|
||||
* accessed by {@link #getPlaybackState()} is only ever changed as a result of operations
|
||||
* completing on the playback thread, as illustrated below.</p>
|
||||
* <p align="center"><img src="../../../../../doc_src/images/exoplayer_state.png"
|
||||
* alt="ExoPlayer state"
|
||||
* border="0"/></p>
|
||||
*
|
||||
* <p>The possible playback state transitions are shown below. Transitions can be triggered either
|
||||
* by changes in the state of the {@link TrackRenderer}s being used, or as a result of
|
||||
* {@link #prepare(TrackRenderer[])}, {@link #stop()} or {@link #release()} being invoked.</p>
|
||||
* <p align="center"><img src="../../../../../doc_src/images/exoplayer_playbackstate.png"
|
||||
* alt="ExoPlayer playback state transitions"
|
||||
* border="0"/></p>
|
||||
*/
|
||||
public interface ExoPlayer {
|
||||
|
||||
/**
|
||||
* A factory for instantiating ExoPlayer instances.
|
||||
*/
|
||||
public static final class Factory {
|
||||
|
||||
/**
|
||||
* The default minimum duration of data that must be buffered for playback to start or resume
|
||||
* following a user action such as a seek.
|
||||
*/
|
||||
public static final int DEFAULT_MIN_BUFFER_MS = 500;
|
||||
|
||||
/**
|
||||
* The default minimum duration of data that must be buffered for playback to resume
|
||||
* after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
|
||||
* not due to a user action such as starting playback or seeking).
|
||||
*/
|
||||
public static final int DEFAULT_MIN_REBUFFER_MS = 5000;
|
||||
|
||||
private Factory() {}
|
||||
|
||||
/**
|
||||
* Obtains an {@link ExoPlayer} instance.
|
||||
* <p>
|
||||
* Must be invoked from a thread that has an associated {@link Looper}.
|
||||
*
|
||||
* @param rendererCount The number of {@link TrackRenderer}s that will be passed to
|
||||
* {@link #prepare(TrackRenderer[])}.
|
||||
* @param minBufferMs A minimum duration of data that must be buffered for playback to start
|
||||
* or resume following a user action such as a seek.
|
||||
* @param minRebufferMs A minimum duration of data that must be buffered for playback to resume
|
||||
* after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
|
||||
* not due to a user action such as starting playback or seeking).
|
||||
*/
|
||||
public static ExoPlayer newInstance(int rendererCount, int minBufferMs, int minRebufferMs) {
|
||||
return new ExoPlayerImpl(rendererCount, minBufferMs, minRebufferMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains an {@link ExoPlayer} instance.
|
||||
* <p>
|
||||
* Must be invoked from a thread that has an associated {@link Looper}.
|
||||
*
|
||||
* @param rendererCount The number of {@link TrackRenderer}s that will be passed to
|
||||
* {@link #prepare(TrackRenderer[])}.
|
||||
*/
|
||||
public static ExoPlayer newInstance(int rendererCount) {
|
||||
return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Please use {@link #newInstance(int, int, int)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static ExoPlayer newInstance(int rendererCount, int minRebufferMs) {
|
||||
return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, minRebufferMs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be notified of changes in player state.
|
||||
*/
|
||||
public interface Listener {
|
||||
/**
|
||||
* Invoked when the value returned from either {@link ExoPlayer#getPlayWhenReady()} or
|
||||
* {@link ExoPlayer#getPlaybackState()} changes.
|
||||
*
|
||||
* @param playWhenReady Whether playback will proceed when ready.
|
||||
* @param playbackState One of the {@code STATE} constants defined in this class.
|
||||
*/
|
||||
void onPlayerStateChanged(boolean playWhenReady, int playbackState);
|
||||
/**
|
||||
* Invoked when the current value of {@link ExoPlayer#getPlayWhenReady()} has been reflected
|
||||
* by the internal playback thread.
|
||||
* <p>
|
||||
* An invocation of this method will shortly follow any call to
|
||||
* {@link ExoPlayer#setPlayWhenReady(boolean)} that changes the state. If multiple calls are
|
||||
* made in rapid succession, then this method will be invoked only once, after the final state
|
||||
* has been reflected.
|
||||
*/
|
||||
void onPlayWhenReadyCommitted();
|
||||
/**
|
||||
* Invoked when an error occurs. The playback state will transition to
|
||||
* {@link ExoPlayer#STATE_IDLE} immediately after this method is invoked. The player instance
|
||||
* can still be used, and {@link ExoPlayer#release()} must still be called on the player should
|
||||
* it no longer be required.
|
||||
*
|
||||
* @param error The error.
|
||||
*/
|
||||
void onPlayerError(ExoPlaybackException error);
|
||||
}
|
||||
|
||||
/**
|
||||
* A component of an {@link ExoPlayer} that can receive messages on the playback thread.
|
||||
* <p>
|
||||
* Messages can be delivered to a component via {@link ExoPlayer#sendMessage} and
|
||||
* {@link ExoPlayer#blockingSendMessage}.
|
||||
*/
|
||||
public interface ExoPlayerComponent {
|
||||
|
||||
/**
|
||||
* Handles a message delivered to the component. Invoked on the playback thread.
|
||||
*
|
||||
* @param messageType An integer identifying the type of message.
|
||||
* @param message The message object.
|
||||
* @throws ExoPlaybackException If an error occurred whilst handling the message.
|
||||
*/
|
||||
void handleMessage(int messageType, Object message) throws ExoPlaybackException;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The player is neither prepared or being prepared.
|
||||
*/
|
||||
public static final int STATE_IDLE = 1;
|
||||
/**
|
||||
* The player is being prepared.
|
||||
*/
|
||||
public static final int STATE_PREPARING = 2;
|
||||
/**
|
||||
* The player is prepared but not able to immediately play from the current position. The cause
|
||||
* is {@link TrackRenderer} specific, but this state typically occurs when more data needs
|
||||
* to be buffered for playback to start.
|
||||
*/
|
||||
public static final int STATE_BUFFERING = 3;
|
||||
/**
|
||||
* The player is prepared and able to immediately play from the current position. The player will
|
||||
* be playing if {@link #setPlayWhenReady(boolean)} returns true, and paused otherwise.
|
||||
*/
|
||||
public static final int STATE_READY = 4;
|
||||
/**
|
||||
* The player has finished playing the media.
|
||||
*/
|
||||
public static final int STATE_ENDED = 5;
|
||||
/**
|
||||
* Represents an unknown time or duration.
|
||||
*/
|
||||
public static final int UNKNOWN_TIME = -1;
|
||||
|
||||
/**
|
||||
* Gets the {@link Looper} associated with the playback thread.
|
||||
*
|
||||
* @return The {@link Looper} associated with the playback thread.
|
||||
*/
|
||||
public Looper getPlaybackLooper();
|
||||
|
||||
/**
|
||||
* Register a listener to receive events from the player. The listener's methods will be invoked
|
||||
* on the thread that was used to construct the player.
|
||||
*
|
||||
* @param listener The listener to register.
|
||||
*/
|
||||
public void addListener(Listener listener);
|
||||
|
||||
/**
|
||||
* Unregister a listener. The listener will no longer receive events from the player.
|
||||
*
|
||||
* @param listener The listener to unregister.
|
||||
*/
|
||||
public void removeListener(Listener listener);
|
||||
|
||||
/**
|
||||
* Returns the current state of the player.
|
||||
*
|
||||
* @return One of the {@code STATE} constants defined in this class.
|
||||
*/
|
||||
public int getPlaybackState();
|
||||
|
||||
/**
|
||||
* Prepares the player for playback.
|
||||
*
|
||||
* @param renderers The {@link TrackRenderer}s to use. The number of renderers must match the
|
||||
* value that was passed to the {@link ExoPlayer.Factory#newInstance} method.
|
||||
*/
|
||||
public void prepare(TrackRenderer... renderers);
|
||||
|
||||
/**
|
||||
* Sets whether the renderer at the given index is enabled.
|
||||
*
|
||||
* @param index The index of the renderer.
|
||||
* @param enabled Whether the renderer at the given index should be enabled.
|
||||
*/
|
||||
public void setRendererEnabled(int index, boolean enabled);
|
||||
|
||||
/**
|
||||
* Whether the renderer at the given index is enabled.
|
||||
*
|
||||
* @param index The index of the renderer.
|
||||
* @return Whether the renderer is enabled.
|
||||
*/
|
||||
public boolean getRendererEnabled(int index);
|
||||
|
||||
/**
|
||||
* Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
|
||||
* If the player is already in this state, then this method can be used to pause and resume
|
||||
* playback.
|
||||
*
|
||||
* @param playWhenReady Whether playback should proceed when ready.
|
||||
*/
|
||||
public void setPlayWhenReady(boolean playWhenReady);
|
||||
|
||||
/**
|
||||
* Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
|
||||
*
|
||||
* @return Whether playback will proceed when ready.
|
||||
*/
|
||||
public boolean getPlayWhenReady();
|
||||
|
||||
/**
|
||||
* Whether the current value of {@link ExoPlayer#getPlayWhenReady()} has been reflected by the
|
||||
* internal playback thread.
|
||||
*
|
||||
* @return True if the current value has been reflected. False otherwise.
|
||||
*/
|
||||
public boolean isPlayWhenReadyCommitted();
|
||||
|
||||
/**
|
||||
* Seeks to a position specified in milliseconds.
|
||||
*
|
||||
* @param positionMs The seek position.
|
||||
*/
|
||||
public void seekTo(int positionMs);
|
||||
|
||||
/**
|
||||
* Stops playback.
|
||||
* <p>
|
||||
* Calling this method will cause the playback state to transition to
|
||||
* {@link ExoPlayer#STATE_IDLE}. Note that the player instance can still be used, and that
|
||||
* {@link ExoPlayer#release()} must still be called on the player should it no longer be required.
|
||||
* <p>
|
||||
* Use {@code setPlayWhenReady(false)} rather than this method if the intention is to pause
|
||||
* playback.
|
||||
*/
|
||||
public void stop();
|
||||
|
||||
/**
|
||||
* Releases the player. This method must be called when the player is no longer required.
|
||||
* <p>
|
||||
* The player must not be used after calling this method.
|
||||
*/
|
||||
public void release();
|
||||
|
||||
/**
|
||||
* Sends a message to a specified component. The message is delivered to the component on the
|
||||
* playback thread. If the component throws a {@link ExoPlaybackException}, then it is
|
||||
* propagated out of the player as an error.
|
||||
*
|
||||
* @param target The target to which the message should be delivered.
|
||||
* @param messageType An integer that can be used to identify the type of the message.
|
||||
* @param message The message object.
|
||||
*/
|
||||
public void sendMessage(ExoPlayerComponent target, int messageType, Object message);
|
||||
|
||||
/**
|
||||
* Blocking variant of {@link #sendMessage(ExoPlayerComponent, int, Object)} that does not return
|
||||
* until after the message has been delivered.
|
||||
*
|
||||
* @param target The target to which the message should be delivered.
|
||||
* @param messageType An integer that can be used to identify the type of the message.
|
||||
* @param message The message object.
|
||||
*/
|
||||
public void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message);
|
||||
|
||||
/**
|
||||
* Gets the duration of the track in milliseconds.
|
||||
*
|
||||
* @return The duration of the track in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME} if the
|
||||
* duration is not known.
|
||||
*/
|
||||
public int getDuration();
|
||||
|
||||
/**
|
||||
* Gets the current playback position in milliseconds.
|
||||
*
|
||||
* @return The current playback position in milliseconds.
|
||||
*/
|
||||
public int getCurrentPosition();
|
||||
|
||||
/**
|
||||
* Gets an estimate of the absolute position in milliseconds up to which data is buffered.
|
||||
*
|
||||
* @return An estimate of the absolute position in milliseconds up to which data is buffered,
|
||||
* or {@link ExoPlayer#UNKNOWN_TIME} if no estimate is available.
|
||||
*/
|
||||
public int getBufferedPosition();
|
||||
|
||||
/**
|
||||
* Gets an estimate of the percentage into the media up to which data is buffered.
|
||||
*
|
||||
* @return An estimate of the percentage into the media up to which data is buffered. 0 if the
|
||||
* duration of the media is not known or if no estimate is available.
|
||||
*/
|
||||
public int getBufferedPercentage();
|
||||
|
||||
}
|
@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
/**
|
||||
* Concrete implementation of {@link ExoPlayer}.
|
||||
*/
|
||||
/* package */ final class ExoPlayerImpl implements ExoPlayer {
|
||||
|
||||
private static final String TAG = "ExoPlayerImpl";
|
||||
|
||||
private final Handler eventHandler;
|
||||
private final ExoPlayerImplInternal internalPlayer;
|
||||
private final CopyOnWriteArraySet<Listener> listeners;
|
||||
private final boolean[] rendererEnabledFlags;
|
||||
|
||||
private boolean playWhenReady;
|
||||
private int playbackState;
|
||||
private int pendingPlayWhenReadyAcks;
|
||||
|
||||
/**
|
||||
* Constructs an instance. Must be invoked from a thread that has an associated {@link Looper}.
|
||||
*
|
||||
* @param rendererCount The number of {@link TrackRenderer}s that will be passed to
|
||||
* {@link #prepare(TrackRenderer[])}.
|
||||
* @param minBufferMs A minimum duration of data that must be buffered for playback to start
|
||||
* or resume following a user action such as a seek.
|
||||
* @param minRebufferMs A minimum duration of data that must be buffered for playback to resume
|
||||
* after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
|
||||
* not due to a user action such as starting playback or seeking).
|
||||
*/
|
||||
@SuppressLint("HandlerLeak")
|
||||
public ExoPlayerImpl(int rendererCount, int minBufferMs, int minRebufferMs) {
|
||||
Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION);
|
||||
this.playbackState = STATE_IDLE;
|
||||
this.listeners = new CopyOnWriteArraySet<Listener>();
|
||||
this.rendererEnabledFlags = new boolean[rendererCount];
|
||||
for (int i = 0; i < rendererEnabledFlags.length; i++) {
|
||||
rendererEnabledFlags[i] = true;
|
||||
}
|
||||
eventHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
ExoPlayerImpl.this.handleEvent(msg);
|
||||
}
|
||||
};
|
||||
internalPlayer = new ExoPlayerImplInternal(eventHandler, playWhenReady, rendererEnabledFlags,
|
||||
minBufferMs, minRebufferMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Looper getPlaybackLooper() {
|
||||
return internalPlayer.getPlaybackLooper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(Listener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(Listener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPlaybackState() {
|
||||
return playbackState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(TrackRenderer... renderers) {
|
||||
internalPlayer.prepare(renderers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRendererEnabled(int index, boolean enabled) {
|
||||
if (rendererEnabledFlags[index] != enabled) {
|
||||
rendererEnabledFlags[index] = enabled;
|
||||
internalPlayer.setRendererEnabled(index, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getRendererEnabled(int index) {
|
||||
return rendererEnabledFlags[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayWhenReady(boolean playWhenReady) {
|
||||
if (this.playWhenReady != playWhenReady) {
|
||||
this.playWhenReady = playWhenReady;
|
||||
pendingPlayWhenReadyAcks++;
|
||||
internalPlayer.setPlayWhenReady(playWhenReady);
|
||||
for (Listener listener : listeners) {
|
||||
listener.onPlayerStateChanged(playWhenReady, playbackState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getPlayWhenReady() {
|
||||
return playWhenReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlayWhenReadyCommitted() {
|
||||
return pendingPlayWhenReadyAcks == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(int positionMs) {
|
||||
internalPlayer.seekTo(positionMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
internalPlayer.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
internalPlayer.release();
|
||||
eventHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(ExoPlayerComponent target, int messageType, Object message) {
|
||||
internalPlayer.sendMessage(target, messageType, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message) {
|
||||
internalPlayer.blockingSendMessage(target, messageType, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDuration() {
|
||||
return internalPlayer.getDuration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentPosition() {
|
||||
return internalPlayer.getCurrentPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBufferedPosition() {
|
||||
return internalPlayer.getBufferedPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBufferedPercentage() {
|
||||
int bufferedPosition = getBufferedPosition();
|
||||
int duration = getDuration();
|
||||
return bufferedPosition == ExoPlayer.UNKNOWN_TIME || duration == ExoPlayer.UNKNOWN_TIME ? 0
|
||||
: (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
|
||||
}
|
||||
|
||||
// Not private so it can be called from an inner class without going through a thunk method.
|
||||
/* package */ void handleEvent(Message msg) {
|
||||
switch (msg.what) {
|
||||
case ExoPlayerImplInternal.MSG_STATE_CHANGED: {
|
||||
playbackState = msg.arg1;
|
||||
for (Listener listener : listeners) {
|
||||
listener.onPlayerStateChanged(playWhenReady, playbackState);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ExoPlayerImplInternal.MSG_SET_PLAY_WHEN_READY_ACK: {
|
||||
pendingPlayWhenReadyAcks--;
|
||||
if (pendingPlayWhenReadyAcks == 0) {
|
||||
for (Listener listener : listeners) {
|
||||
listener.onPlayWhenReadyCommitted();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ExoPlayerImplInternal.MSG_ERROR: {
|
||||
ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
|
||||
for (Listener listener : listeners) {
|
||||
listener.onPlayerError(exception);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,579 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.TraceUtil;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Process;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implements the internal behavior of {@link ExoPlayerImpl}.
|
||||
*/
|
||||
/* package */ final class ExoPlayerImplInternal implements Handler.Callback {
|
||||
|
||||
private static final String TAG = "ExoPlayerImplInternal";
|
||||
|
||||
// External messages
|
||||
public static final int MSG_STATE_CHANGED = 1;
|
||||
public static final int MSG_SET_PLAY_WHEN_READY_ACK = 2;
|
||||
public static final int MSG_ERROR = 3;
|
||||
|
||||
// Internal messages
|
||||
private static final int MSG_PREPARE = 1;
|
||||
private static final int MSG_INCREMENTAL_PREPARE = 2;
|
||||
private static final int MSG_SET_PLAY_WHEN_READY = 3;
|
||||
private static final int MSG_STOP = 4;
|
||||
private static final int MSG_RELEASE = 5;
|
||||
private static final int MSG_SEEK_TO = 6;
|
||||
private static final int MSG_DO_SOME_WORK = 7;
|
||||
private static final int MSG_SET_RENDERER_ENABLED = 8;
|
||||
private static final int MSG_CUSTOM = 9;
|
||||
|
||||
private static final int PREPARE_INTERVAL_MS = 10;
|
||||
private static final int RENDERING_INTERVAL_MS = 10;
|
||||
private static final int IDLE_INTERVAL_MS = 1000;
|
||||
|
||||
private final Handler handler;
|
||||
private final HandlerThread internalPlayerThread;
|
||||
private final Handler eventHandler;
|
||||
private final MediaClock mediaClock;
|
||||
private final boolean[] rendererEnabledFlags;
|
||||
private final long minBufferUs;
|
||||
private final long minRebufferUs;
|
||||
|
||||
private final List<TrackRenderer> enabledRenderers;
|
||||
private TrackRenderer[] renderers;
|
||||
private TrackRenderer timeSourceTrackRenderer;
|
||||
|
||||
private boolean released;
|
||||
private boolean playWhenReady;
|
||||
private boolean rebuffering;
|
||||
private int state;
|
||||
private int customMessagesSent = 0;
|
||||
private int customMessagesProcessed = 0;
|
||||
|
||||
private volatile long durationUs;
|
||||
private volatile long positionUs;
|
||||
private volatile long bufferedPositionUs;
|
||||
|
||||
@SuppressLint("HandlerLeak")
|
||||
public ExoPlayerImplInternal(Handler eventHandler, boolean playWhenReady,
|
||||
boolean[] rendererEnabledFlags, int minBufferMs, int minRebufferMs) {
|
||||
this.eventHandler = eventHandler;
|
||||
this.playWhenReady = playWhenReady;
|
||||
this.rendererEnabledFlags = new boolean[rendererEnabledFlags.length];
|
||||
this.minBufferUs = minBufferMs * 1000L;
|
||||
this.minRebufferUs = minRebufferMs * 1000L;
|
||||
for (int i = 0; i < rendererEnabledFlags.length; i++) {
|
||||
this.rendererEnabledFlags[i] = rendererEnabledFlags[i];
|
||||
}
|
||||
|
||||
this.state = ExoPlayer.STATE_IDLE;
|
||||
this.durationUs = TrackRenderer.UNKNOWN_TIME;
|
||||
this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME;
|
||||
|
||||
mediaClock = new MediaClock();
|
||||
enabledRenderers = new ArrayList<TrackRenderer>(rendererEnabledFlags.length);
|
||||
internalPlayerThread = new HandlerThread(getClass().getSimpleName() + ":Handler") {
|
||||
@Override
|
||||
public void run() {
|
||||
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
|
||||
// not normally change to this priority" is incorrect.
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO);
|
||||
super.run();
|
||||
}
|
||||
};
|
||||
internalPlayerThread.start();
|
||||
handler = new Handler(internalPlayerThread.getLooper(), this);
|
||||
}
|
||||
|
||||
public Looper getPlaybackLooper() {
|
||||
return internalPlayerThread.getLooper();
|
||||
}
|
||||
|
||||
public int getCurrentPosition() {
|
||||
return (int) (positionUs / 1000);
|
||||
}
|
||||
|
||||
public int getBufferedPosition() {
|
||||
return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME ? ExoPlayer.UNKNOWN_TIME
|
||||
: (int) (bufferedPositionUs / 1000);
|
||||
}
|
||||
|
||||
public int getDuration() {
|
||||
return durationUs == TrackRenderer.UNKNOWN_TIME ? ExoPlayer.UNKNOWN_TIME
|
||||
: (int) (durationUs / 1000);
|
||||
}
|
||||
|
||||
public void prepare(TrackRenderer... renderers) {
|
||||
handler.obtainMessage(MSG_PREPARE, renderers).sendToTarget();
|
||||
}
|
||||
|
||||
public void setPlayWhenReady(boolean playWhenReady) {
|
||||
handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();
|
||||
}
|
||||
|
||||
public void seekTo(int positionMs) {
|
||||
handler.obtainMessage(MSG_SEEK_TO, positionMs, 0).sendToTarget();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
handler.sendEmptyMessage(MSG_STOP);
|
||||
}
|
||||
|
||||
public void setRendererEnabled(int index, boolean enabled) {
|
||||
handler.obtainMessage(MSG_SET_RENDERER_ENABLED, index, enabled ? 1 : 0).sendToTarget();
|
||||
}
|
||||
|
||||
public void sendMessage(ExoPlayerComponent target, int messageType, Object message) {
|
||||
customMessagesSent++;
|
||||
handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget();
|
||||
}
|
||||
|
||||
public synchronized void blockingSendMessage(ExoPlayerComponent target, int messageType,
|
||||
Object message) {
|
||||
int messageNumber = customMessagesSent++;
|
||||
handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget();
|
||||
while (customMessagesProcessed <= messageNumber) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void release() {
|
||||
if (!released) {
|
||||
handler.sendEmptyMessage(MSG_RELEASE);
|
||||
while (!released) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
internalPlayerThread.quit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(Message msg) {
|
||||
try {
|
||||
switch (msg.what) {
|
||||
case MSG_PREPARE: {
|
||||
prepareInternal((TrackRenderer[]) msg.obj);
|
||||
return true;
|
||||
}
|
||||
case MSG_INCREMENTAL_PREPARE: {
|
||||
incrementalPrepareInternal();
|
||||
return true;
|
||||
}
|
||||
case MSG_SET_PLAY_WHEN_READY: {
|
||||
setPlayWhenReadyInternal(msg.arg1 != 0);
|
||||
return true;
|
||||
}
|
||||
case MSG_DO_SOME_WORK: {
|
||||
doSomeWork();
|
||||
return true;
|
||||
}
|
||||
case MSG_SEEK_TO: {
|
||||
seekToInternal(msg.arg1);
|
||||
return true;
|
||||
}
|
||||
case MSG_STOP: {
|
||||
stopInternal();
|
||||
return true;
|
||||
}
|
||||
case MSG_RELEASE: {
|
||||
releaseInternal();
|
||||
return true;
|
||||
}
|
||||
case MSG_CUSTOM: {
|
||||
sendMessageInternal(msg.arg1, msg.obj);
|
||||
return true;
|
||||
}
|
||||
case MSG_SET_RENDERER_ENABLED: {
|
||||
setRendererEnabledInternal(msg.arg1, msg.arg2 != 0);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} catch (ExoPlaybackException e) {
|
||||
Log.e(TAG, "Internal track renderer error.", e);
|
||||
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
|
||||
stopInternal();
|
||||
return true;
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "Internal runtime error.", e);
|
||||
eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e)).sendToTarget();
|
||||
stopInternal();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void setState(int state) {
|
||||
if (this.state != state) {
|
||||
this.state = state;
|
||||
eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareInternal(TrackRenderer[] renderers) {
|
||||
rebuffering = false;
|
||||
this.renderers = renderers;
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
if (renderers[i].isTimeSource()) {
|
||||
Assertions.checkState(timeSourceTrackRenderer == null);
|
||||
timeSourceTrackRenderer = renderers[i];
|
||||
}
|
||||
}
|
||||
setState(ExoPlayer.STATE_PREPARING);
|
||||
handler.sendEmptyMessage(MSG_INCREMENTAL_PREPARE);
|
||||
}
|
||||
|
||||
private void incrementalPrepareInternal() throws ExoPlaybackException {
|
||||
long operationStartTimeMs = SystemClock.elapsedRealtime();
|
||||
boolean prepared = true;
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
if (renderers[i].getState() == TrackRenderer.STATE_UNPREPARED) {
|
||||
int state = renderers[i].prepare();
|
||||
if (state == TrackRenderer.STATE_UNPREPARED) {
|
||||
prepared = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!prepared) {
|
||||
// We're still waiting for some sources to be prepared.
|
||||
scheduleNextOperation(MSG_INCREMENTAL_PREPARE, operationStartTimeMs, PREPARE_INTERVAL_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
long durationUs = 0;
|
||||
boolean isEnded = true;
|
||||
boolean allRenderersReadyOrEnded = true;
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
TrackRenderer renderer = renderers[i];
|
||||
if (rendererEnabledFlags[i] && renderer.getState() == TrackRenderer.STATE_PREPARED) {
|
||||
renderer.enable(positionUs, false);
|
||||
enabledRenderers.add(renderer);
|
||||
isEnded = isEnded && renderer.isEnded();
|
||||
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
|
||||
if (durationUs == TrackRenderer.UNKNOWN_TIME) {
|
||||
// We've already encountered a track for which the duration is unknown, so the media
|
||||
// duration is unknown regardless of the duration of this track.
|
||||
} else {
|
||||
long trackDurationUs = renderer.getDurationUs();
|
||||
if (trackDurationUs == TrackRenderer.UNKNOWN_TIME) {
|
||||
durationUs = TrackRenderer.UNKNOWN_TIME;
|
||||
} else if (trackDurationUs == TrackRenderer.MATCH_LONGEST) {
|
||||
// Do nothing.
|
||||
} else {
|
||||
durationUs = Math.max(durationUs, trackDurationUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.durationUs = durationUs;
|
||||
|
||||
if (isEnded) {
|
||||
// We don't expect this case, but handle it anyway.
|
||||
setState(ExoPlayer.STATE_ENDED);
|
||||
} else {
|
||||
setState(allRenderersReadyOrEnded ? ExoPlayer.STATE_READY : ExoPlayer.STATE_BUFFERING);
|
||||
if (playWhenReady && state == ExoPlayer.STATE_READY) {
|
||||
startRenderers();
|
||||
}
|
||||
}
|
||||
|
||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||
}
|
||||
|
||||
private boolean rendererReadyOrEnded(TrackRenderer renderer) {
|
||||
if (renderer.isEnded()) {
|
||||
return true;
|
||||
}
|
||||
if (!renderer.isReady()) {
|
||||
return false;
|
||||
}
|
||||
if (state == ExoPlayer.STATE_READY) {
|
||||
return true;
|
||||
}
|
||||
long rendererDurationUs = renderer.getDurationUs();
|
||||
long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
|
||||
long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs;
|
||||
return minBufferDurationUs <= 0
|
||||
|| rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME
|
||||
|| rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK
|
||||
|| rendererBufferedPositionUs >= positionUs + minBufferDurationUs
|
||||
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME
|
||||
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST
|
||||
&& rendererBufferedPositionUs >= rendererDurationUs);
|
||||
}
|
||||
|
||||
private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
|
||||
try {
|
||||
rebuffering = false;
|
||||
this.playWhenReady = playWhenReady;
|
||||
if (!playWhenReady) {
|
||||
stopRenderers();
|
||||
updatePositionUs();
|
||||
} else {
|
||||
if (state == ExoPlayer.STATE_READY) {
|
||||
startRenderers();
|
||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||
} else if (state == ExoPlayer.STATE_BUFFERING) {
|
||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
eventHandler.obtainMessage(MSG_SET_PLAY_WHEN_READY_ACK).sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
private void startRenderers() throws ExoPlaybackException {
|
||||
rebuffering = false;
|
||||
mediaClock.start();
|
||||
for (int i = 0; i < enabledRenderers.size(); i++) {
|
||||
enabledRenderers.get(i).start();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopRenderers() throws ExoPlaybackException {
|
||||
mediaClock.stop();
|
||||
for (int i = 0; i < enabledRenderers.size(); i++) {
|
||||
ensureStopped(enabledRenderers.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePositionUs() {
|
||||
positionUs = timeSourceTrackRenderer != null &&
|
||||
enabledRenderers.contains(timeSourceTrackRenderer) ?
|
||||
timeSourceTrackRenderer.getCurrentPositionUs() :
|
||||
mediaClock.getTimeUs();
|
||||
}
|
||||
|
||||
private void doSomeWork() throws ExoPlaybackException {
|
||||
TraceUtil.beginSection("doSomeWork");
|
||||
long operationStartTimeMs = SystemClock.elapsedRealtime();
|
||||
long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME ? durationUs
|
||||
: Long.MAX_VALUE;
|
||||
boolean isEnded = true;
|
||||
boolean allRenderersReadyOrEnded = true;
|
||||
updatePositionUs();
|
||||
for (int i = 0; i < enabledRenderers.size(); i++) {
|
||||
TrackRenderer renderer = enabledRenderers.get(i);
|
||||
// TODO: Each renderer should return the maximum delay before which it wishes to be
|
||||
// invoked again. The minimum of these values should then be used as the delay before the next
|
||||
// invocation of this method.
|
||||
renderer.doSomeWork(positionUs);
|
||||
isEnded = isEnded && renderer.isEnded();
|
||||
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
|
||||
|
||||
if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME) {
|
||||
// We've already encountered a track for which the buffered position is unknown. Hence the
|
||||
// media buffer position unknown regardless of the buffered position of this track.
|
||||
} else {
|
||||
long rendererDurationUs = renderer.getDurationUs();
|
||||
long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
|
||||
if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME) {
|
||||
bufferedPositionUs = TrackRenderer.UNKNOWN_TIME;
|
||||
} else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK
|
||||
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME
|
||||
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST
|
||||
&& rendererBufferedPositionUs >= rendererDurationUs)) {
|
||||
// This track is fully buffered.
|
||||
} else {
|
||||
bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.bufferedPositionUs = bufferedPositionUs;
|
||||
|
||||
if (isEnded) {
|
||||
setState(ExoPlayer.STATE_ENDED);
|
||||
stopRenderers();
|
||||
} else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded) {
|
||||
setState(ExoPlayer.STATE_READY);
|
||||
if (playWhenReady) {
|
||||
startRenderers();
|
||||
}
|
||||
} else if (state == ExoPlayer.STATE_READY && !allRenderersReadyOrEnded) {
|
||||
rebuffering = playWhenReady;
|
||||
setState(ExoPlayer.STATE_BUFFERING);
|
||||
stopRenderers();
|
||||
}
|
||||
|
||||
handler.removeMessages(MSG_DO_SOME_WORK);
|
||||
if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
|
||||
scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, RENDERING_INTERVAL_MS);
|
||||
} else if (!enabledRenderers.isEmpty()) {
|
||||
scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, IDLE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
TraceUtil.endSection();
|
||||
}
|
||||
|
||||
private void scheduleNextOperation(int operationType, long thisOperationStartTimeMs,
|
||||
long intervalMs) {
|
||||
long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
|
||||
long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
|
||||
if (nextOperationDelayMs <= 0) {
|
||||
handler.sendEmptyMessage(operationType);
|
||||
} else {
|
||||
handler.sendEmptyMessageDelayed(operationType, nextOperationDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
private void seekToInternal(int positionMs) throws ExoPlaybackException {
|
||||
rebuffering = false;
|
||||
positionUs = positionMs * 1000L;
|
||||
mediaClock.stop();
|
||||
mediaClock.setTimeUs(positionUs);
|
||||
if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < enabledRenderers.size(); i++) {
|
||||
TrackRenderer renderer = enabledRenderers.get(i);
|
||||
ensureStopped(renderer);
|
||||
renderer.seekTo(positionUs);
|
||||
}
|
||||
setState(ExoPlayer.STATE_BUFFERING);
|
||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||
}
|
||||
|
||||
private void stopInternal() {
|
||||
rebuffering = false;
|
||||
resetInternal();
|
||||
}
|
||||
|
||||
private void releaseInternal() {
|
||||
resetInternal();
|
||||
synchronized (this) {
|
||||
released = true;
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void resetInternal() {
|
||||
handler.removeMessages(MSG_DO_SOME_WORK);
|
||||
handler.removeMessages(MSG_INCREMENTAL_PREPARE);
|
||||
mediaClock.stop();
|
||||
if (renderers == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
try {
|
||||
TrackRenderer renderer = renderers[i];
|
||||
ensureStopped(renderer);
|
||||
if (renderer.getState() == TrackRenderer.STATE_ENABLED) {
|
||||
renderer.disable();
|
||||
}
|
||||
renderer.release();
|
||||
} catch (ExoPlaybackException e) {
|
||||
// There's nothing we can do. Catch the exception here so that other renderers still have
|
||||
// a chance of being cleaned up correctly.
|
||||
Log.e(TAG, "Stop failed.", e);
|
||||
} catch (RuntimeException e) {
|
||||
// Ditto.
|
||||
Log.e(TAG, "Stop failed.", e);
|
||||
}
|
||||
}
|
||||
renderers = null;
|
||||
timeSourceTrackRenderer = null;
|
||||
enabledRenderers.clear();
|
||||
setState(ExoPlayer.STATE_IDLE);
|
||||
}
|
||||
|
||||
private <T> void sendMessageInternal(int what, Object obj)
|
||||
throws ExoPlaybackException {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Pair<ExoPlayerComponent, Object> targetAndMessage = (Pair<ExoPlayerComponent, Object>) obj;
|
||||
targetAndMessage.first.handleMessage(what, targetAndMessage.second);
|
||||
} finally {
|
||||
synchronized (this) {
|
||||
customMessagesProcessed++;
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
if (state != ExoPlayer.STATE_IDLE) {
|
||||
// The message may have caused something to change that now requires us to do work.
|
||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||
}
|
||||
}
|
||||
|
||||
private void setRendererEnabledInternal(int index, boolean enabled)
|
||||
throws ExoPlaybackException {
|
||||
if (rendererEnabledFlags[index] == enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
rendererEnabledFlags[index] = enabled;
|
||||
if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) {
|
||||
return;
|
||||
}
|
||||
|
||||
TrackRenderer renderer = renderers[index];
|
||||
int rendererState = renderer.getState();
|
||||
if (rendererState != TrackRenderer.STATE_PREPARED &&
|
||||
rendererState != TrackRenderer.STATE_ENABLED &&
|
||||
rendererState != TrackRenderer.STATE_STARTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
|
||||
renderer.enable(positionUs, playing);
|
||||
enabledRenderers.add(renderer);
|
||||
if (playing) {
|
||||
renderer.start();
|
||||
}
|
||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||
} else {
|
||||
if (renderer == timeSourceTrackRenderer) {
|
||||
// We've been using timeSourceTrackRenderer to advance the current position, but it's
|
||||
// being disabled. Sync mediaClock so that it can take over timing responsibilities.
|
||||
mediaClock.setTimeUs(renderer.getCurrentPositionUs());
|
||||
}
|
||||
ensureStopped(renderer);
|
||||
enabledRenderers.remove(renderer);
|
||||
renderer.disable();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureStopped(TrackRenderer renderer) throws ExoPlaybackException {
|
||||
if (renderer.getState() == TrackRenderer.STATE_STARTED) {
|
||||
renderer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Information about the ExoPlayer library.
|
||||
*/
|
||||
// TODO: This file should be automatically generated by the build system.
|
||||
public class ExoPlayerLibraryInfo {
|
||||
|
||||
private ExoPlayerLibraryInfo() {}
|
||||
|
||||
/**
|
||||
* The version of the library, expressed as a string.
|
||||
*/
|
||||
public static final String VERSION = "1.0.10";
|
||||
|
||||
/**
|
||||
* The version of the library, expressed as an integer.
|
||||
* <p>
|
||||
* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
|
||||
* corresponding integer version 1002003.
|
||||
*/
|
||||
public static final int VERSION_INT = 1000010;
|
||||
|
||||
/**
|
||||
* Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}
|
||||
* checks enabled.
|
||||
*/
|
||||
public static final boolean ASSERTIONS_ENABLED = true;
|
||||
|
||||
/**
|
||||
* Whether the library was compiled with {@link com.google.android.exoplayer.util.TraceUtil}
|
||||
* trace enabled.
|
||||
*/
|
||||
public static final boolean TRACE_ENABLED = true;
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
|
||||
*/
|
||||
public final class FormatHolder {
|
||||
|
||||
/**
|
||||
* The format of the media.
|
||||
*/
|
||||
public MediaFormat format;
|
||||
/**
|
||||
* Initialization data for each of the drm schemes supported by the media, keyed by scheme UUID.
|
||||
* Null if the media is not encrypted.
|
||||
*/
|
||||
public Map<UUID, byte[]> drmInitData;
|
||||
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.media.MediaExtractor;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Extracts samples from a stream using Android's {@link MediaExtractor}.
|
||||
*/
|
||||
// TODO: This implementation needs to be fixed so that its methods are non-blocking (either
|
||||
// through use of a background thread, or through changes to the framework's MediaExtractor API).
|
||||
@TargetApi(16)
|
||||
public final class FrameworkSampleSource implements SampleSource {
|
||||
|
||||
private static final int TRACK_STATE_DISABLED = 0;
|
||||
private static final int TRACK_STATE_ENABLED = 1;
|
||||
private static final int TRACK_STATE_FORMAT_SENT = 2;
|
||||
|
||||
private final Context context;
|
||||
private final Uri uri;
|
||||
private final Map<String, String> headers;
|
||||
|
||||
private MediaExtractor extractor;
|
||||
private TrackInfo[] trackInfos;
|
||||
private boolean prepared;
|
||||
private int remainingReleaseCount;
|
||||
private int[] trackStates;
|
||||
private boolean[] pendingDiscontinuities;
|
||||
|
||||
private long seekTimeUs;
|
||||
|
||||
public FrameworkSampleSource(Context context, Uri uri, Map<String, String> headers,
|
||||
int downstreamRendererCount) {
|
||||
Assertions.checkState(Util.SDK_INT >= 16);
|
||||
this.context = context;
|
||||
this.uri = uri;
|
||||
this.headers = headers;
|
||||
this.remainingReleaseCount = downstreamRendererCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare() throws IOException {
|
||||
if (!prepared) {
|
||||
extractor = new MediaExtractor();
|
||||
extractor.setDataSource(context, uri, headers);
|
||||
trackStates = new int[extractor.getTrackCount()];
|
||||
pendingDiscontinuities = new boolean[extractor.getTrackCount()];
|
||||
trackInfos = new TrackInfo[trackStates.length];
|
||||
for (int i = 0; i < trackStates.length; i++) {
|
||||
android.media.MediaFormat format = extractor.getTrackFormat(i);
|
||||
long duration = format.containsKey(android.media.MediaFormat.KEY_DURATION) ?
|
||||
format.getLong(android.media.MediaFormat.KEY_DURATION) : TrackRenderer.UNKNOWN_TIME;
|
||||
String mime = format.getString(android.media.MediaFormat.KEY_MIME);
|
||||
trackInfos[i] = new TrackInfo(mime, duration);
|
||||
}
|
||||
prepared = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTrackCount() {
|
||||
Assertions.checkState(prepared);
|
||||
return extractor.getTrackCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackInfo getTrackInfo(int track) {
|
||||
Assertions.checkState(prepared);
|
||||
return trackInfos[track];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable(int track, long timeUs) {
|
||||
Assertions.checkState(prepared);
|
||||
Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED);
|
||||
boolean wasSourceEnabled = isEnabled();
|
||||
trackStates[track] = TRACK_STATE_ENABLED;
|
||||
extractor.selectTrack(track);
|
||||
if (!wasSourceEnabled) {
|
||||
seekToUs(timeUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void continueBuffering(long playbackPositionUs) {
|
||||
// Do nothing. The MediaExtractor instance is responsible for buffering.
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
|
||||
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) {
|
||||
Assertions.checkState(prepared);
|
||||
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
|
||||
if (pendingDiscontinuities[track]) {
|
||||
pendingDiscontinuities[track] = false;
|
||||
return DISCONTINUITY_READ;
|
||||
}
|
||||
if (onlyReadDiscontinuity) {
|
||||
return NOTHING_READ;
|
||||
}
|
||||
int extractorTrackIndex = extractor.getSampleTrackIndex();
|
||||
if (extractorTrackIndex == track) {
|
||||
if (trackStates[track] != TRACK_STATE_FORMAT_SENT) {
|
||||
formatHolder.format = MediaFormat.createFromFrameworkMediaFormatV16(
|
||||
extractor.getTrackFormat(track));
|
||||
formatHolder.drmInitData = Util.SDK_INT >= 18 ? getPsshInfoV18() : null;
|
||||
trackStates[track] = TRACK_STATE_FORMAT_SENT;
|
||||
return FORMAT_READ;
|
||||
}
|
||||
if (sampleHolder.data != null) {
|
||||
int offset = sampleHolder.data.position();
|
||||
sampleHolder.size = extractor.readSampleData(sampleHolder.data, offset);
|
||||
sampleHolder.data.position(offset + sampleHolder.size);
|
||||
} else {
|
||||
sampleHolder.size = 0;
|
||||
}
|
||||
sampleHolder.timeUs = extractor.getSampleTime();
|
||||
sampleHolder.flags = extractor.getSampleFlags();
|
||||
if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) {
|
||||
sampleHolder.cryptoInfo.setFromExtractorV16(extractor);
|
||||
}
|
||||
seekTimeUs = -1;
|
||||
extractor.advance();
|
||||
return SAMPLE_READ;
|
||||
} else {
|
||||
return extractorTrackIndex < 0 ? END_OF_STREAM : NOTHING_READ;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(18)
|
||||
private Map<UUID, byte[]> getPsshInfoV18() {
|
||||
Map<UUID, byte[]> psshInfo = extractor.getPsshInfo();
|
||||
return (psshInfo == null || psshInfo.isEmpty()) ? null : psshInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable(int track) {
|
||||
Assertions.checkState(prepared);
|
||||
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
|
||||
extractor.unselectTrack(track);
|
||||
pendingDiscontinuities[track] = false;
|
||||
trackStates[track] = TRACK_STATE_DISABLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToUs(long timeUs) {
|
||||
Assertions.checkState(prepared);
|
||||
if (seekTimeUs != timeUs) {
|
||||
// Avoid duplicate calls to the underlying extractor's seek method in the case that there
|
||||
// have been no interleaving calls to advance.
|
||||
seekTimeUs = timeUs;
|
||||
extractor.seekTo(timeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
|
||||
for (int i = 0; i < trackStates.length; ++i) {
|
||||
if (trackStates[i] != TRACK_STATE_DISABLED) {
|
||||
pendingDiscontinuities[i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPositionUs() {
|
||||
Assertions.checkState(prepared);
|
||||
long bufferedDurationUs = extractor.getCachedDuration();
|
||||
if (bufferedDurationUs == -1) {
|
||||
return TrackRenderer.UNKNOWN_TIME;
|
||||
} else {
|
||||
return extractor.getSampleTime() + bufferedDurationUs;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
Assertions.checkState(remainingReleaseCount > 0);
|
||||
if (--remainingReleaseCount == 0) {
|
||||
extractor.release();
|
||||
extractor = null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isEnabled() {
|
||||
for (int i = 0; i < trackStates.length; i++) {
|
||||
if (trackStates[i] != TRACK_STATE_DISABLED) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.upstream.Allocator;
|
||||
|
||||
/**
|
||||
* Coordinates multiple loaders of time series data.
|
||||
*/
|
||||
public interface LoadControl {
|
||||
|
||||
/**
|
||||
* Registers a loader.
|
||||
*
|
||||
* @param loader The loader being registered.
|
||||
* @param bufferSizeContribution For controls whose {@link Allocator}s maintain a pool of memory
|
||||
* for the purpose of satisfying allocation requests, this is a hint indicating the loader's
|
||||
* desired contribution to the size of the pool, in bytes.
|
||||
*/
|
||||
void register(Object loader, int bufferSizeContribution);
|
||||
|
||||
/**
|
||||
* Unregisters a loader.
|
||||
*
|
||||
* @param loader The loader being unregistered.
|
||||
*/
|
||||
void unregister(Object loader);
|
||||
|
||||
/**
|
||||
* Gets the {@link Allocator} that loaders should use to obtain memory allocations into which
|
||||
* data can be loaded.
|
||||
*
|
||||
* @return The {@link Allocator} to use.
|
||||
*/
|
||||
Allocator getAllocator();
|
||||
|
||||
/**
|
||||
* Hints to the control that it should consider trimming any unused memory being held in order
|
||||
* to satisfy allocation requests.
|
||||
* <p>
|
||||
* This method is typically invoked by a recently unregistered loader, once it has released all
|
||||
* of its allocations back to the {@link Allocator}.
|
||||
*/
|
||||
void trimAllocator();
|
||||
|
||||
/**
|
||||
* Invoked by a loader to update the control with its current state.
|
||||
* <p>
|
||||
* This method must be called by a registered loader whenever its state changes. This is true
|
||||
* even if the registered loader does not itself wish to start its next load (since the state of
|
||||
* the loader will still affect whether other registered loaders are allowed to proceed).
|
||||
*
|
||||
* @param loader The loader invoking the update.
|
||||
* @param playbackPositionUs The loader's playback position.
|
||||
* @param nextLoadPositionUs The loader's next load position, or -1 if finished.
|
||||
* @param loading Whether the loader is currently loading data.
|
||||
* @param failed Whether the loader has failed, meaning it does not wish to load more data.
|
||||
* @return True if the loader is allowed to start its next load. False otherwise.
|
||||
*/
|
||||
boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs,
|
||||
boolean loading, boolean failed);
|
||||
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
/**
|
||||
* A simple clock for tracking the progression of media time. The clock can be started, stopped and
|
||||
* its time can be set and retrieved. When started, this clock is based on
|
||||
* {@link SystemClock#elapsedRealtime()}.
|
||||
*/
|
||||
/* package */ class MediaClock {
|
||||
|
||||
private boolean started;
|
||||
|
||||
/**
|
||||
* The media time when the clock was last set or stopped.
|
||||
*/
|
||||
private long timeUs;
|
||||
|
||||
/**
|
||||
* The difference between {@link SystemClock#elapsedRealtime()} and {@link #timeUs}
|
||||
* when the clock was last set or started.
|
||||
*/
|
||||
private long deltaUs;
|
||||
|
||||
/**
|
||||
* Starts the clock. Does nothing if the clock is already started.
|
||||
*/
|
||||
public void start() {
|
||||
if (!started) {
|
||||
started = true;
|
||||
deltaUs = elapsedRealtimeMinus(timeUs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the clock. Does nothing if the clock is already stopped.
|
||||
*/
|
||||
public void stop() {
|
||||
if (started) {
|
||||
timeUs = elapsedRealtimeMinus(deltaUs);
|
||||
started = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param timeUs The time to set in microseconds.
|
||||
*/
|
||||
public void setTimeUs(long timeUs) {
|
||||
this.timeUs = timeUs;
|
||||
deltaUs = elapsedRealtimeMinus(timeUs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current time in microseconds.
|
||||
*/
|
||||
public long getTimeUs() {
|
||||
return started ? elapsedRealtimeMinus(deltaUs) : timeUs;
|
||||
}
|
||||
|
||||
private long elapsedRealtimeMinus(long microSeconds) {
|
||||
return SystemClock.elapsedRealtime() * 1000 - microSeconds;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,733 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTimestamp;
|
||||
import android.media.AudioTrack;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.ConditionVariable;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be notified of {@link MediaCodecAudioTrackRenderer}
|
||||
* events.
|
||||
*/
|
||||
public interface EventListener extends MediaCodecTrackRenderer.EventListener {
|
||||
|
||||
/**
|
||||
* Invoked when an {@link AudioTrack} fails to initialize.
|
||||
*
|
||||
* @param e The corresponding exception.
|
||||
*/
|
||||
void onAudioTrackInitializationError(AudioTrackInitializationException e);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a failure occurs instantiating an audio track.
|
||||
*/
|
||||
public static class AudioTrackInitializationException extends Exception {
|
||||
|
||||
/**
|
||||
* The state as reported by {@link AudioTrack#getState()}
|
||||
*/
|
||||
public final int audioTrackState;
|
||||
|
||||
public AudioTrackInitializationException(int audioTrackState, int sampleRate,
|
||||
int channelConfig, int bufferSize) {
|
||||
super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", " +
|
||||
channelConfig + ", " + bufferSize + ")");
|
||||
this.audioTrackState = audioTrackState;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of a message that can be passed to an instance of this class via
|
||||
* {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object
|
||||
* should be a {@link Float} with 0 being silence and 1 being unity gain.
|
||||
*/
|
||||
public static final int MSG_SET_VOLUME = 1;
|
||||
|
||||
/**
|
||||
* The default multiplication factor used when determining the size of the underlying
|
||||
* {@link AudioTrack}'s buffer.
|
||||
*/
|
||||
public static final float DEFAULT_MIN_BUFFER_MULTIPLICATION_FACTOR = 4;
|
||||
|
||||
private static final String TAG = "MediaCodecAudioTrackRenderer";
|
||||
|
||||
private static final long MICROS_PER_SECOND = 1000000L;
|
||||
|
||||
private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10;
|
||||
private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
|
||||
private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
|
||||
|
||||
private final EventListener eventListener;
|
||||
private final ConditionVariable audioTrackReleasingConditionVariable;
|
||||
private final AudioTimestampCompat audioTimestampCompat;
|
||||
private final long[] playheadOffsets;
|
||||
private final float minBufferMultiplicationFactor;
|
||||
private int nextPlayheadOffsetIndex;
|
||||
private int playheadOffsetCount;
|
||||
private long smoothedPlayheadOffsetUs;
|
||||
private long lastPlayheadSampleTimeUs;
|
||||
private boolean audioTimestampSet;
|
||||
private long lastTimestampSampleTimeUs;
|
||||
private long lastRawPlaybackHeadPosition;
|
||||
private long rawPlaybackHeadWrapCount;
|
||||
|
||||
private int sampleRate;
|
||||
private int frameSize;
|
||||
private int channelConfig;
|
||||
private int minBufferSize;
|
||||
private int bufferSize;
|
||||
|
||||
private AudioTrack audioTrack;
|
||||
private Method audioTrackGetLatencyMethod;
|
||||
private int audioSessionId;
|
||||
private long submittedBytes;
|
||||
private boolean audioTrackStartMediaTimeSet;
|
||||
private long audioTrackStartMediaTimeUs;
|
||||
private long audioTrackResumeSystemTimeUs;
|
||||
private long lastReportedCurrentPositionUs;
|
||||
private long audioTrackLatencyUs;
|
||||
private float volume;
|
||||
|
||||
private byte[] temporaryBuffer;
|
||||
private int temporaryBufferOffset;
|
||||
private int temporaryBufferSize;
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
*/
|
||||
public MediaCodecAudioTrackRenderer(SampleSource source) {
|
||||
this(source, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
|
||||
* content is not required.
|
||||
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
|
||||
* For example a media file may start with a short clear region so as to allow playback to
|
||||
* begin in parallel with key acquisision. This parameter specifies whether the renderer is
|
||||
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||
*/
|
||||
public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys) {
|
||||
this(source, drmSessionManager, playClearSamplesWithoutKeys, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
*/
|
||||
public MediaCodecAudioTrackRenderer(SampleSource source, Handler eventHandler,
|
||||
EventListener eventListener) {
|
||||
this(source, null, true, eventHandler, eventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
|
||||
* content is not required.
|
||||
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
|
||||
* For example a media file may start with a short clear region so as to allow playback to
|
||||
* begin in parallel with key acquisision. This parameter specifies whether the renderer is
|
||||
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
*/
|
||||
public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) {
|
||||
this(source, drmSessionManager, playClearSamplesWithoutKeys,
|
||||
DEFAULT_MIN_BUFFER_MULTIPLICATION_FACTOR, eventHandler, eventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
* @param minBufferMultiplicationFactor When instantiating an underlying {@link AudioTrack},
|
||||
* the size of the track's is calculated as this value multiplied by the minimum buffer size
|
||||
* obtained from {@link AudioTrack#getMinBufferSize(int, int, int)}. The multiplication
|
||||
* factor must be greater than or equal to 1.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
*/
|
||||
public MediaCodecAudioTrackRenderer(SampleSource source, float minBufferMultiplicationFactor,
|
||||
Handler eventHandler, EventListener eventListener) {
|
||||
this(source, null, true, minBufferMultiplicationFactor, eventHandler, eventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
|
||||
* content is not required.
|
||||
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
|
||||
* For example a media file may start with a short clear region so as to allow playback to
|
||||
* begin in parallel with key acquisision. This parameter specifies whether the renderer is
|
||||
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||
* @param minBufferMultiplicationFactor When instantiating an underlying {@link AudioTrack},
|
||||
* the size of the track's is calculated as this value multiplied by the minimum buffer size
|
||||
* obtained from {@link AudioTrack#getMinBufferSize(int, int, int)}. The multiplication
|
||||
* factor must be greater than or equal to 1.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
*/
|
||||
public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys, float minBufferMultiplicationFactor,
|
||||
Handler eventHandler, EventListener eventListener) {
|
||||
super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener);
|
||||
Assertions.checkState(minBufferMultiplicationFactor >= 1);
|
||||
this.minBufferMultiplicationFactor = minBufferMultiplicationFactor;
|
||||
this.eventListener = eventListener;
|
||||
audioTrackReleasingConditionVariable = new ConditionVariable(true);
|
||||
if (Util.SDK_INT >= 19) {
|
||||
audioTimestampCompat = new AudioTimestampCompatV19();
|
||||
} else {
|
||||
audioTimestampCompat = new NoopAudioTimestampCompat();
|
||||
}
|
||||
if (Util.SDK_INT >= 18) {
|
||||
try {
|
||||
audioTrackGetLatencyMethod = AudioTrack.class.getMethod("getLatency", (Class<?>[]) null);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// There's no guarantee this method exists. Do nothing.
|
||||
}
|
||||
}
|
||||
playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
|
||||
volume = 1.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isTimeSource() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean handlesMimeType(String mimeType) {
|
||||
return MimeTypes.isAudio(mimeType) && super.handlesMimeType(mimeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnabled(long timeUs, boolean joining) {
|
||||
super.onEnabled(timeUs, joining);
|
||||
lastReportedCurrentPositionUs = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
||||
super.doSomeWork(timeUs);
|
||||
maybeSampleSyncParams();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onOutputFormatChanged(MediaFormat format) {
|
||||
releaseAudioTrack();
|
||||
this.sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
||||
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
||||
int channelConfig;
|
||||
switch (channelCount) {
|
||||
case 1:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||
break;
|
||||
case 2:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
break;
|
||||
case 6:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported channel count: " + channelCount);
|
||||
}
|
||||
this.channelConfig = channelConfig;
|
||||
this.minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT);
|
||||
this.bufferSize = (int) (minBufferMultiplicationFactor * minBufferSize);
|
||||
this.frameSize = 2 * channelCount; // 2 bytes per 16 bit sample * number of channels.
|
||||
}
|
||||
|
||||
private void initAudioTrack() throws ExoPlaybackException {
|
||||
// If we're asynchronously releasing a previous audio track then we block until it has been
|
||||
// released. This guarantees that we cannot end up in a state where we have multiple audio
|
||||
// track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
|
||||
// the shared memory that's available for audio track buffers. This would in turn cause the
|
||||
// initialization of the audio track to fail.
|
||||
audioTrackReleasingConditionVariable.block();
|
||||
if (audioSessionId == 0) {
|
||||
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM);
|
||||
checkAudioTrackInitialized();
|
||||
audioSessionId = audioTrack.getAudioSessionId();
|
||||
onAudioSessionId(audioSessionId);
|
||||
} else {
|
||||
// Re-attach to the same audio session.
|
||||
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM, audioSessionId);
|
||||
checkAudioTrackInitialized();
|
||||
}
|
||||
audioTrack.setStereoVolume(volume, volume);
|
||||
if (getState() == TrackRenderer.STATE_STARTED) {
|
||||
audioTrackResumeSystemTimeUs = System.nanoTime() / 1000;
|
||||
audioTrack.play();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this
|
||||
* method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an
|
||||
* exception is thrown.
|
||||
*
|
||||
* @throws ExoPlaybackException If {@link #audioTrack} has not been successfully initialized.
|
||||
*/
|
||||
private void checkAudioTrackInitialized() throws ExoPlaybackException {
|
||||
int audioTrackState = audioTrack.getState();
|
||||
if (audioTrackState == AudioTrack.STATE_INITIALIZED) {
|
||||
return;
|
||||
}
|
||||
// The track is not successfully initialized. Release and null the track.
|
||||
try {
|
||||
audioTrack.release();
|
||||
} catch (Exception e) {
|
||||
// The track has already failed to initialize, so it wouldn't be that surprising if release
|
||||
// were to fail too. Swallow the exception.
|
||||
} finally {
|
||||
audioTrack = null;
|
||||
}
|
||||
// Propagate the relevant exceptions.
|
||||
AudioTrackInitializationException exception = new AudioTrackInitializationException(
|
||||
audioTrackState, sampleRate, channelConfig, bufferSize);
|
||||
notifyAudioTrackInitializationError(exception);
|
||||
throw new ExoPlaybackException(exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the audio session id becomes known. Once the id is known it will not change
|
||||
* (and hence this method will not be invoked again) unless the renderer is disabled and then
|
||||
* subsequently re-enabled.
|
||||
* <p>
|
||||
* The default implementation is a no-op. One reason for overriding this method would be to
|
||||
* instantiate and enable a {@link android.media.audiofx.Virtualizer} in order to spatialize the
|
||||
* audio channels. For this use case, any {@link android.media.audiofx.Virtualizer} instances
|
||||
* should be released in {@link #onDisabled()} (if not before).
|
||||
*
|
||||
* @param audioSessionId The audio session id.
|
||||
*/
|
||||
protected void onAudioSessionId(int audioSessionId) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void releaseAudioTrack() {
|
||||
if (audioTrack != null) {
|
||||
submittedBytes = 0;
|
||||
temporaryBufferSize = 0;
|
||||
lastRawPlaybackHeadPosition = 0;
|
||||
rawPlaybackHeadWrapCount = 0;
|
||||
audioTrackStartMediaTimeUs = 0;
|
||||
audioTrackStartMediaTimeSet = false;
|
||||
resetSyncParams();
|
||||
int playState = audioTrack.getPlayState();
|
||||
if (playState == AudioTrack.PLAYSTATE_PLAYING) {
|
||||
audioTrack.pause();
|
||||
}
|
||||
// AudioTrack.release can take some time, so we call it on a background thread.
|
||||
final AudioTrack toRelease = audioTrack;
|
||||
audioTrack = null;
|
||||
audioTrackReleasingConditionVariable.close();
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
toRelease.release();
|
||||
} finally {
|
||||
audioTrackReleasingConditionVariable.open();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStarted() {
|
||||
super.onStarted();
|
||||
if (audioTrack != null) {
|
||||
audioTrackResumeSystemTimeUs = System.nanoTime() / 1000;
|
||||
audioTrack.play();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStopped() {
|
||||
super.onStopped();
|
||||
if (audioTrack != null) {
|
||||
resetSyncParams();
|
||||
audioTrack.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isEnded() {
|
||||
// We've exhausted the output stream, and the AudioTrack has either played all of the data
|
||||
// submitted, or has been fed insufficient data to begin playback.
|
||||
return super.isEnded() && (getPendingFrameCount() == 0 || submittedBytes < minBufferSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isReady() {
|
||||
return getPendingFrameCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method uses a variety of techniques to compute the current position:
|
||||
*
|
||||
* 1. Prior to playback having started, calls up to the super class to obtain the pending seek
|
||||
* position.
|
||||
* 2. During playback, uses AudioTimestamps obtained from AudioTrack.getTimestamp on supported
|
||||
* devices.
|
||||
* 3. Else, derives a smoothed position by sampling the AudioTrack's frame position.
|
||||
*/
|
||||
@Override
|
||||
protected long getCurrentPositionUs() {
|
||||
long systemClockUs = System.nanoTime() / 1000;
|
||||
long currentPositionUs;
|
||||
if (audioTrack == null || !audioTrackStartMediaTimeSet) {
|
||||
// The AudioTrack hasn't started.
|
||||
currentPositionUs = super.getCurrentPositionUs();
|
||||
} else if (audioTimestampSet) {
|
||||
// How long ago in the past the audio timestamp is (negative if it's in the future)
|
||||
long presentationDiff = systemClockUs - (audioTimestampCompat.getNanoTime() / 1000);
|
||||
long framesDiff = durationUsToFrames(presentationDiff);
|
||||
// The position of the frame that's currently being presented.
|
||||
long currentFramePosition = audioTimestampCompat.getFramePosition() + framesDiff;
|
||||
currentPositionUs = framesToDurationUs(currentFramePosition) + audioTrackStartMediaTimeUs;
|
||||
} else {
|
||||
if (playheadOffsetCount == 0) {
|
||||
// The AudioTrack has started, but we don't have any samples to compute a smoothed position.
|
||||
currentPositionUs = getPlayheadPositionUs() + audioTrackStartMediaTimeUs;
|
||||
} else {
|
||||
// getPlayheadPositionUs() only has a granularity of ~20ms, so we base the position off the
|
||||
// system clock (and a smoothed offset between it and the playhead position) so as to
|
||||
// prevent jitter in the reported positions.
|
||||
currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + audioTrackStartMediaTimeUs;
|
||||
}
|
||||
if (!isEnded()) {
|
||||
currentPositionUs -= audioTrackLatencyUs;
|
||||
}
|
||||
}
|
||||
// Make sure we don't ever report time moving backwards as a result of smoothing or switching
|
||||
// between the various code paths above.
|
||||
currentPositionUs = Math.max(lastReportedCurrentPositionUs, currentPositionUs);
|
||||
lastReportedCurrentPositionUs = currentPositionUs;
|
||||
return currentPositionUs;
|
||||
}
|
||||
|
||||
private void maybeSampleSyncParams() {
|
||||
if (audioTrack == null || !audioTrackStartMediaTimeSet || getState() != STATE_STARTED) {
|
||||
// The AudioTrack isn't playing.
|
||||
return;
|
||||
}
|
||||
long playheadPositionUs = getPlayheadPositionUs();
|
||||
if (playheadPositionUs == 0) {
|
||||
// The AudioTrack hasn't output anything yet.
|
||||
return;
|
||||
}
|
||||
long systemClockUs = System.nanoTime() / 1000;
|
||||
if (systemClockUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) {
|
||||
// Take a new sample and update the smoothed offset between the system clock and the playhead.
|
||||
playheadOffsets[nextPlayheadOffsetIndex] = playheadPositionUs - systemClockUs;
|
||||
nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT;
|
||||
if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) {
|
||||
playheadOffsetCount++;
|
||||
}
|
||||
lastPlayheadSampleTimeUs = systemClockUs;
|
||||
smoothedPlayheadOffsetUs = 0;
|
||||
for (int i = 0; i < playheadOffsetCount; i++) {
|
||||
smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
|
||||
audioTimestampSet = audioTimestampCompat.initTimestamp(audioTrack);
|
||||
if (audioTimestampSet
|
||||
&& (audioTimestampCompat.getNanoTime() / 1000) < audioTrackResumeSystemTimeUs) {
|
||||
// The timestamp was set, but it corresponds to a time before the track was most recently
|
||||
// resumed.
|
||||
audioTimestampSet = false;
|
||||
}
|
||||
if (audioTrackGetLatencyMethod != null) {
|
||||
try {
|
||||
// Compute the audio track latency, excluding the latency due to the buffer (leaving
|
||||
// latency due to the mixer and audio hardware driver).
|
||||
audioTrackLatencyUs =
|
||||
(Integer) audioTrackGetLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L -
|
||||
framesToDurationUs(bufferSize / frameSize);
|
||||
// Sanity check that the latency is non-negative.
|
||||
audioTrackLatencyUs = Math.max(audioTrackLatencyUs, 0);
|
||||
} catch (Exception e) {
|
||||
// The method existed, but doesn't work. Don't try again.
|
||||
audioTrackGetLatencyMethod = null;
|
||||
}
|
||||
}
|
||||
lastTimestampSampleTimeUs = systemClockUs;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetSyncParams() {
|
||||
smoothedPlayheadOffsetUs = 0;
|
||||
playheadOffsetCount = 0;
|
||||
nextPlayheadOffsetIndex = 0;
|
||||
lastPlayheadSampleTimeUs = 0;
|
||||
audioTimestampSet = false;
|
||||
lastTimestampSampleTimeUs = 0;
|
||||
}
|
||||
|
||||
private long getPlayheadPositionUs() {
|
||||
return framesToDurationUs(getPlaybackHeadPosition());
|
||||
}
|
||||
|
||||
private long framesToDurationUs(long frameCount) {
|
||||
return (frameCount * MICROS_PER_SECOND) / sampleRate;
|
||||
}
|
||||
|
||||
private long durationUsToFrames(long durationUs) {
|
||||
return (durationUs * sampleRate) / MICROS_PER_SECOND;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisabled() {
|
||||
super.onDisabled();
|
||||
releaseAudioTrack();
|
||||
audioSessionId = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seekTo(long timeUs) throws ExoPlaybackException {
|
||||
super.seekTo(timeUs);
|
||||
// TODO: Try and re-use the same AudioTrack instance once [redacted] is fixed.
|
||||
releaseAudioTrack();
|
||||
lastReportedCurrentPositionUs = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
|
||||
MediaCodec.BufferInfo bufferInfo, int bufferIndex) throws ExoPlaybackException {
|
||||
if (temporaryBufferSize == 0) {
|
||||
// This is the first time we've seen this {@code buffer}.
|
||||
|
||||
// Note: presentationTimeUs corresponds to the end of the sample, not the start.
|
||||
long bufferStartTime = bufferInfo.presentationTimeUs -
|
||||
framesToDurationUs(bufferInfo.size / frameSize);
|
||||
if (!audioTrackStartMediaTimeSet) {
|
||||
audioTrackStartMediaTimeUs = Math.max(0, bufferStartTime);
|
||||
audioTrackStartMediaTimeSet = true;
|
||||
} else {
|
||||
// Sanity check that bufferStartTime is consistent with the expected value.
|
||||
long expectedBufferStartTime = audioTrackStartMediaTimeUs +
|
||||
framesToDurationUs(submittedBytes / frameSize);
|
||||
if (Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) {
|
||||
Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " +
|
||||
bufferStartTime + "]");
|
||||
// Adjust audioTrackStartMediaTimeUs to compensate for the discontinuity. Also reset
|
||||
// lastReportedCurrentPositionUs to allow time to jump backwards if it really wants to.
|
||||
audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime);
|
||||
lastReportedCurrentPositionUs = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy {@code buffer} into {@code temporaryBuffer}.
|
||||
// TODO: Bypass this copy step on versions of Android where [redacted] is implemented.
|
||||
if (temporaryBuffer == null || temporaryBuffer.length < bufferInfo.size) {
|
||||
temporaryBuffer = new byte[bufferInfo.size];
|
||||
}
|
||||
buffer.position(bufferInfo.offset);
|
||||
buffer.get(temporaryBuffer, 0, bufferInfo.size);
|
||||
temporaryBufferOffset = 0;
|
||||
temporaryBufferSize = bufferInfo.size;
|
||||
}
|
||||
|
||||
if (audioTrack == null) {
|
||||
initAudioTrack();
|
||||
}
|
||||
|
||||
// TODO: Don't bother doing this once [redacted] is fixed.
|
||||
// Work out how many bytes we can write without the risk of blocking.
|
||||
int bytesPending = (int) (submittedBytes - getPlaybackHeadPosition() * frameSize);
|
||||
int bytesToWrite = bufferSize - bytesPending;
|
||||
|
||||
if (bytesToWrite > 0) {
|
||||
bytesToWrite = Math.min(temporaryBufferSize, bytesToWrite);
|
||||
audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite);
|
||||
temporaryBufferOffset += bytesToWrite;
|
||||
temporaryBufferSize -= bytesToWrite;
|
||||
submittedBytes += bytesToWrite;
|
||||
if (temporaryBufferSize == 0) {
|
||||
codec.releaseOutputBuffer(bufferIndex, false);
|
||||
codecCounters.renderedOutputBufferCount++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as
|
||||
* an unsigned 32 bit integer, which also wraps around periodically. This method returns the
|
||||
* playback head position as a long that will only wrap around if the value exceeds
|
||||
* {@link Long#MAX_VALUE} (which in practice will never happen).
|
||||
*
|
||||
* @return {@link AudioTrack#getPlaybackHeadPosition()} of {@link #audioTrack} expressed as a
|
||||
* long.
|
||||
*/
|
||||
private long getPlaybackHeadPosition() {
|
||||
long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();
|
||||
if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) {
|
||||
// The value must have wrapped around.
|
||||
rawPlaybackHeadWrapCount++;
|
||||
}
|
||||
lastRawPlaybackHeadPosition = rawPlaybackHeadPosition;
|
||||
return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);
|
||||
}
|
||||
|
||||
private int getPendingFrameCount() {
|
||||
return audioTrack == null ?
|
||||
0 : (int) (submittedBytes / frameSize - getPlaybackHeadPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
|
||||
if (messageType == MSG_SET_VOLUME) {
|
||||
setVolume((Float) message);
|
||||
} else {
|
||||
super.handleMessage(messageType, message);
|
||||
}
|
||||
}
|
||||
|
||||
private void setVolume(float volume) {
|
||||
this.volume = volume;
|
||||
if (audioTrack != null) {
|
||||
audioTrack.setStereoVolume(volume, volume);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyAudioTrackInitializationError(final AudioTrackInitializationException e) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onAudioTrackInitializationError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface exposing the {@link AudioTimestamp} methods we need that were added in SDK 19.
|
||||
*/
|
||||
private interface AudioTimestampCompat {
|
||||
|
||||
/**
|
||||
* Returns true if the audioTimestamp was retrieved from the audioTrack.
|
||||
*/
|
||||
boolean initTimestamp(AudioTrack audioTrack);
|
||||
|
||||
long getNanoTime();
|
||||
|
||||
long getFramePosition();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The AudioTimestampCompat implementation for SDK < 19 that does nothing or throws an exception.
|
||||
*/
|
||||
private static final class NoopAudioTimestampCompat implements AudioTimestampCompat {
|
||||
|
||||
@Override
|
||||
public boolean initTimestamp(AudioTrack audioTrack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNanoTime() {
|
||||
// Should never be called if initTimestamp() returned false.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFramePosition() {
|
||||
// Should never be called if initTimestamp() returned false.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The AudioTimestampCompat implementation for SDK >= 19 that simply calls through to the actual
|
||||
* implementations added in SDK 19.
|
||||
*/
|
||||
@TargetApi(19)
|
||||
private static final class AudioTimestampCompatV19 implements AudioTimestampCompat {
|
||||
|
||||
private final AudioTimestamp audioTimestamp;
|
||||
|
||||
public AudioTimestampCompatV19() {
|
||||
audioTimestamp = new AudioTimestamp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initTimestamp(AudioTrack audioTrack) {
|
||||
return audioTrack.getTimestamp(audioTimestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNanoTime() {
|
||||
return audioTimestamp.nanoTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFramePosition() {
|
||||
return audioTimestamp.framePosition;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,735 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodec.CryptoException;
|
||||
import android.media.MediaCrypto;
|
||||
import android.media.MediaExtractor;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* An abstract {@link TrackRenderer} that uses {@link MediaCodec} to decode samples for rendering.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be notified of {@link MediaCodecTrackRenderer} events.
|
||||
*/
|
||||
public interface EventListener {
|
||||
|
||||
/**
|
||||
* Invoked when a decoder fails to initialize.
|
||||
*
|
||||
* @param e The corresponding exception.
|
||||
*/
|
||||
void onDecoderInitializationError(DecoderInitializationException e);
|
||||
|
||||
/**
|
||||
* Invoked when a decoder operation raises a {@link CryptoException}.
|
||||
*
|
||||
* @param e The corresponding exception.
|
||||
*/
|
||||
void onCryptoError(CryptoException e);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a failure occurs instantiating a decoder.
|
||||
*/
|
||||
public static class DecoderInitializationException extends Exception {
|
||||
|
||||
/**
|
||||
* The name of the decoder that failed to initialize.
|
||||
*/
|
||||
public final String decoderName;
|
||||
|
||||
public DecoderInitializationException(String decoderName, MediaFormat mediaFormat,
|
||||
Exception cause) {
|
||||
super("Decoder init failed: " + decoderName + ", " + mediaFormat, cause);
|
||||
this.decoderName = decoderName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of
|
||||
* time during which {@link #isReady()} will report true regardless of whether the new codec has
|
||||
* output frames that are ready to be rendered.
|
||||
* <p>
|
||||
* This allows codec hotswapping to be performed seamlessly, without interrupting the playback of
|
||||
* other renderers, provided the new codec is able to decode some frames within this time period.
|
||||
*/
|
||||
private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000;
|
||||
|
||||
/**
|
||||
* There is no pending adaptive reconfiguration work.
|
||||
*/
|
||||
private static final int RECONFIGURATION_STATE_NONE = 0;
|
||||
/**
|
||||
* Codec configuration data needs to be written into the next buffer.
|
||||
*/
|
||||
private static final int RECONFIGURATION_STATE_WRITE_PENDING = 1;
|
||||
/**
|
||||
* Codec configuration data has been written into the next buffer, but that buffer still needs to
|
||||
* be returned to the codec.
|
||||
*/
|
||||
private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2;
|
||||
|
||||
public final CodecCounters codecCounters;
|
||||
|
||||
private final DrmSessionManager drmSessionManager;
|
||||
private final boolean playClearSamplesWithoutKeys;
|
||||
private final SampleSource source;
|
||||
private final SampleHolder sampleHolder;
|
||||
private final FormatHolder formatHolder;
|
||||
private final HashSet<Long> decodeOnlyPresentationTimestamps;
|
||||
private final MediaCodec.BufferInfo outputBufferInfo;
|
||||
private final EventListener eventListener;
|
||||
protected final Handler eventHandler;
|
||||
|
||||
private MediaFormat format;
|
||||
private Map<UUID, byte[]> drmInitData;
|
||||
private MediaCodec codec;
|
||||
private boolean codecIsAdaptive;
|
||||
private ByteBuffer[] inputBuffers;
|
||||
private ByteBuffer[] outputBuffers;
|
||||
private long codecHotswapTimeMs;
|
||||
private int inputIndex;
|
||||
private int outputIndex;
|
||||
private boolean openedDrmSession;
|
||||
private boolean codecReconfigured;
|
||||
private int codecReconfigurationState;
|
||||
|
||||
private int trackIndex;
|
||||
private boolean inputStreamEnded;
|
||||
private boolean outputStreamEnded;
|
||||
private boolean waitingForKeys;
|
||||
private boolean waitingForFirstSyncFrame;
|
||||
private long currentPositionUs;
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
* @param drmSessionManager For use with encrypted media. May be null if support for encrypted
|
||||
* media is not required.
|
||||
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
|
||||
* For example a media file may start with a short clear region so as to allow playback to
|
||||
* begin in parallel with key acquisision. This parameter specifies whether the renderer is
|
||||
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
*/
|
||||
public MediaCodecTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) {
|
||||
Assertions.checkState(Util.SDK_INT >= 16);
|
||||
this.source = source;
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
codecCounters = new CodecCounters();
|
||||
sampleHolder = new SampleHolder(false);
|
||||
formatHolder = new FormatHolder();
|
||||
decodeOnlyPresentationTimestamps = new HashSet<Long>();
|
||||
outputBufferInfo = new MediaCodec.BufferInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int doPrepare() throws ExoPlaybackException {
|
||||
try {
|
||||
boolean sourcePrepared = source.prepare();
|
||||
if (!sourcePrepared) {
|
||||
return TrackRenderer.STATE_UNPREPARED;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ExoPlaybackException(e);
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.getTrackCount(); i++) {
|
||||
// TODO: Right now this is getting the mime types of the container format
|
||||
// (e.g. audio/mp4 and video/mp4 for fragmented mp4). It needs to be getting the mime types
|
||||
// of the actual samples (e.g. audio/mp4a-latm and video/avc).
|
||||
if (handlesMimeType(source.getTrackInfo(i).mimeType)) {
|
||||
trackIndex = i;
|
||||
return TrackRenderer.STATE_PREPARED;
|
||||
}
|
||||
}
|
||||
|
||||
return TrackRenderer.STATE_IGNORE;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected boolean handlesMimeType(String mimeType) {
|
||||
return true;
|
||||
// TODO: Uncomment once the TODO above is fixed.
|
||||
// DecoderInfoUtil.getDecoder(mimeType) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnabled(long timeUs, boolean joining) {
|
||||
source.enable(trackIndex, timeUs);
|
||||
inputStreamEnded = false;
|
||||
outputStreamEnded = false;
|
||||
waitingForKeys = false;
|
||||
currentPositionUs = timeUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a newly created {@link MediaCodec}. Sub-classes should
|
||||
* override this method if they wish to configure the codec with a
|
||||
* non-null surface.
|
||||
**/
|
||||
protected void configureCodec(MediaCodec codec, android.media.MediaFormat x, MediaCrypto crypto) {
|
||||
codec.configure(x, null, crypto, 0);
|
||||
}
|
||||
|
||||
protected final void maybeInitCodec() throws ExoPlaybackException {
|
||||
if (!shouldInitCodec()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String mimeType = format.mimeType;
|
||||
MediaCrypto mediaCrypto = null;
|
||||
boolean requiresSecureDecoder = false;
|
||||
if (drmInitData != null) {
|
||||
if (drmSessionManager == null) {
|
||||
throw new ExoPlaybackException("Media requires a DrmSessionManager");
|
||||
}
|
||||
if (!openedDrmSession) {
|
||||
drmSessionManager.open(drmInitData, mimeType);
|
||||
openedDrmSession = true;
|
||||
}
|
||||
int drmSessionState = drmSessionManager.getState();
|
||||
if (drmSessionState == DrmSessionManager.STATE_ERROR) {
|
||||
throw new ExoPlaybackException(drmSessionManager.getError());
|
||||
} else if (drmSessionState == DrmSessionManager.STATE_OPENED
|
||||
|| drmSessionState == DrmSessionManager.STATE_OPENED_WITH_KEYS) {
|
||||
mediaCrypto = drmSessionManager.getMediaCrypto();
|
||||
requiresSecureDecoder = drmSessionManager.requiresSecureDecoderComponent(mimeType);
|
||||
} else {
|
||||
// The drm session isn't open yet.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
DecoderInfo selectedDecoderInfo = MediaCodecUtil.getDecoderInfo(mimeType);
|
||||
String selectedDecoderName = selectedDecoderInfo.name;
|
||||
if (requiresSecureDecoder) {
|
||||
selectedDecoderName = getSecureDecoderName(selectedDecoderName);
|
||||
}
|
||||
codecIsAdaptive = selectedDecoderInfo.adaptive;
|
||||
try {
|
||||
codec = MediaCodec.createByCodecName(selectedDecoderName);
|
||||
configureCodec(codec, format.getFrameworkMediaFormatV16(), mediaCrypto);
|
||||
codec.start();
|
||||
inputBuffers = codec.getInputBuffers();
|
||||
outputBuffers = codec.getOutputBuffers();
|
||||
} catch (Exception e) {
|
||||
DecoderInitializationException exception = new DecoderInitializationException(
|
||||
selectedDecoderName, format, e);
|
||||
notifyDecoderInitializationError(exception);
|
||||
throw new ExoPlaybackException(exception);
|
||||
}
|
||||
codecHotswapTimeMs = getState() == TrackRenderer.STATE_STARTED ?
|
||||
SystemClock.elapsedRealtime() : -1;
|
||||
inputIndex = -1;
|
||||
outputIndex = -1;
|
||||
waitingForFirstSyncFrame = true;
|
||||
codecCounters.codecInitCount++;
|
||||
}
|
||||
|
||||
protected boolean shouldInitCodec() {
|
||||
return codec == null && format != null;
|
||||
}
|
||||
|
||||
protected final boolean codecInitialized() {
|
||||
return codec != null;
|
||||
}
|
||||
|
||||
protected final boolean haveFormat() {
|
||||
return format != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisabled() {
|
||||
releaseCodec();
|
||||
format = null;
|
||||
drmInitData = null;
|
||||
if (openedDrmSession) {
|
||||
drmSessionManager.close();
|
||||
openedDrmSession = false;
|
||||
}
|
||||
source.disable(trackIndex);
|
||||
}
|
||||
|
||||
protected void releaseCodec() {
|
||||
if (codec != null) {
|
||||
codecHotswapTimeMs = -1;
|
||||
inputIndex = -1;
|
||||
outputIndex = -1;
|
||||
decodeOnlyPresentationTimestamps.clear();
|
||||
inputBuffers = null;
|
||||
outputBuffers = null;
|
||||
codecReconfigured = false;
|
||||
codecIsAdaptive = false;
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
|
||||
codecCounters.codecReleaseCount++;
|
||||
try {
|
||||
codec.stop();
|
||||
} finally {
|
||||
try {
|
||||
codec.release();
|
||||
} finally {
|
||||
codec = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReleased() {
|
||||
source.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getCurrentPositionUs() {
|
||||
return currentPositionUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getDurationUs() {
|
||||
return source.getTrackInfo(trackIndex).durationUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getBufferedPositionUs() {
|
||||
long sourceBufferedPosition = source.getBufferedPositionUs();
|
||||
return sourceBufferedPosition == UNKNOWN_TIME || sourceBufferedPosition == END_OF_TRACK
|
||||
? sourceBufferedPosition : Math.max(sourceBufferedPosition, getCurrentPositionUs());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seekTo(long timeUs) throws ExoPlaybackException {
|
||||
currentPositionUs = timeUs;
|
||||
source.seekToUs(timeUs);
|
||||
inputStreamEnded = false;
|
||||
outputStreamEnded = false;
|
||||
waitingForKeys = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStarted() {
|
||||
// Do nothing. Overridden to remove throws clause.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStopped() {
|
||||
// Do nothing. Overridden to remove throws clause.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
||||
try {
|
||||
source.continueBuffering(timeUs);
|
||||
checkForDiscontinuity();
|
||||
if (format == null) {
|
||||
readFormat();
|
||||
} else if (codec == null && !shouldInitCodec() && getState() == TrackRenderer.STATE_STARTED) {
|
||||
discardSamples(timeUs);
|
||||
} else {
|
||||
if (codec == null && shouldInitCodec()) {
|
||||
maybeInitCodec();
|
||||
}
|
||||
if (codec != null) {
|
||||
while (drainOutputBuffer(timeUs)) {}
|
||||
while (feedInputBuffer()) {}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ExoPlaybackException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void readFormat() throws IOException, ExoPlaybackException {
|
||||
int result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
||||
if (result == SampleSource.FORMAT_READ) {
|
||||
onInputFormatChanged(formatHolder);
|
||||
}
|
||||
}
|
||||
|
||||
private void discardSamples(long timeUs) throws IOException, ExoPlaybackException {
|
||||
sampleHolder.data = null;
|
||||
int result = SampleSource.SAMPLE_READ;
|
||||
while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) {
|
||||
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
||||
if (result == SampleSource.SAMPLE_READ) {
|
||||
currentPositionUs = sampleHolder.timeUs;
|
||||
codecCounters.discardedSamplesCount++;
|
||||
} else if (result == SampleSource.FORMAT_READ) {
|
||||
onInputFormatChanged(formatHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkForDiscontinuity() throws IOException, ExoPlaybackException {
|
||||
if (codec == null) {
|
||||
return;
|
||||
}
|
||||
int result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, true);
|
||||
if (result == SampleSource.DISCONTINUITY_READ) {
|
||||
flushCodec();
|
||||
}
|
||||
}
|
||||
|
||||
private void flushCodec() throws ExoPlaybackException {
|
||||
codecHotswapTimeMs = -1;
|
||||
inputIndex = -1;
|
||||
outputIndex = -1;
|
||||
decodeOnlyPresentationTimestamps.clear();
|
||||
// Workaround for framework bugs.
|
||||
// See [redacted], [redacted], [redacted].
|
||||
if (Util.SDK_INT >= 18) {
|
||||
codec.flush();
|
||||
} else {
|
||||
releaseCodec();
|
||||
maybeInitCodec();
|
||||
}
|
||||
if (codecReconfigured && format != null) {
|
||||
// Any reconfiguration data that we send shortly before the flush may be discarded. We
|
||||
// avoid this issue by sending reconfiguration data following every flush.
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if it may be possible to feed more input data. False otherwise.
|
||||
* @throws IOException If an error occurs reading data from the upstream source.
|
||||
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
|
||||
*/
|
||||
private boolean feedInputBuffer() throws IOException, ExoPlaybackException {
|
||||
if (inputStreamEnded) {
|
||||
return false;
|
||||
}
|
||||
if (inputIndex < 0) {
|
||||
inputIndex = codec.dequeueInputBuffer(0);
|
||||
if (inputIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
sampleHolder.data = inputBuffers[inputIndex];
|
||||
sampleHolder.data.clear();
|
||||
}
|
||||
|
||||
int result;
|
||||
if (waitingForKeys) {
|
||||
// We've already read an encrypted sample into sampleHolder, and are waiting for keys.
|
||||
result = SampleSource.SAMPLE_READ;
|
||||
} else {
|
||||
// For adaptive reconfiguration OMX decoders expect all reconfiguration data to be supplied
|
||||
// at the start of the buffer that also contains the first frame in the new format.
|
||||
if (codecReconfigurationState == RECONFIGURATION_STATE_WRITE_PENDING) {
|
||||
for (int i = 0; i < format.initializationData.size(); i++) {
|
||||
byte[] data = format.initializationData.get(i);
|
||||
sampleHolder.data.put(data);
|
||||
}
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
|
||||
}
|
||||
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
||||
}
|
||||
|
||||
if (result == SampleSource.NOTHING_READ) {
|
||||
codecCounters.inputBufferWaitingForSampleCount++;
|
||||
return false;
|
||||
}
|
||||
if (result == SampleSource.DISCONTINUITY_READ) {
|
||||
flushCodec();
|
||||
return true;
|
||||
}
|
||||
if (result == SampleSource.FORMAT_READ) {
|
||||
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
|
||||
// We received two formats in a row. Clear the current buffer of any reconfiguration data
|
||||
// associated with the first format.
|
||||
sampleHolder.data.clear();
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
||||
}
|
||||
onInputFormatChanged(formatHolder);
|
||||
return true;
|
||||
}
|
||||
if (result == SampleSource.END_OF_STREAM) {
|
||||
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
|
||||
// We received a new format immediately before the end of the stream. We need to clear
|
||||
// the corresponding reconfiguration data from the current buffer, but re-write it into
|
||||
// a subsequent buffer if there are any (e.g. if the user seeks backwards).
|
||||
sampleHolder.data.clear();
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
||||
}
|
||||
inputStreamEnded = true;
|
||||
try {
|
||||
codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
||||
inputIndex = -1;
|
||||
codecCounters.queuedEndOfStreamCount++;
|
||||
} catch (CryptoException e) {
|
||||
notifyCryptoError(e);
|
||||
throw new ExoPlaybackException(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (waitingForFirstSyncFrame) {
|
||||
// TODO: Find out if it's possible to supply samples prior to the first sync
|
||||
// frame for HE-AAC.
|
||||
if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) == 0) {
|
||||
sampleHolder.data.clear();
|
||||
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
|
||||
// The buffer we just cleared contained reconfiguration data. We need to re-write this
|
||||
// data into a subsequent buffer (if there is one).
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
waitingForFirstSyncFrame = false;
|
||||
}
|
||||
boolean sampleEncrypted = (sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0;
|
||||
waitingForKeys = shouldWaitForKeys(sampleEncrypted);
|
||||
if (waitingForKeys) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
int bufferSize = sampleHolder.data.position();
|
||||
int adaptiveReconfigurationBytes = bufferSize - sampleHolder.size;
|
||||
long presentationTimeUs = sampleHolder.timeUs;
|
||||
if (sampleHolder.decodeOnly) {
|
||||
decodeOnlyPresentationTimestamps.add(presentationTimeUs);
|
||||
}
|
||||
if (sampleEncrypted) {
|
||||
MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(sampleHolder,
|
||||
adaptiveReconfigurationBytes);
|
||||
codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0);
|
||||
} else {
|
||||
codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0);
|
||||
}
|
||||
codecCounters.queuedInputBufferCount++;
|
||||
if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
|
||||
codecCounters.keyframeCount++;
|
||||
}
|
||||
inputIndex = -1;
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
|
||||
} catch (CryptoException e) {
|
||||
notifyCryptoError(e);
|
||||
throw new ExoPlaybackException(e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(SampleHolder sampleHolder,
|
||||
int adaptiveReconfigurationBytes) {
|
||||
MediaCodec.CryptoInfo cryptoInfo = sampleHolder.cryptoInfo.getFrameworkCryptoInfoV16();
|
||||
if (adaptiveReconfigurationBytes == 0) {
|
||||
return cryptoInfo;
|
||||
}
|
||||
// There must be at least one sub-sample, although numBytesOfClearData is permitted to be
|
||||
// null if it contains no clear data. Instantiate it if needed, and add the reconfiguration
|
||||
// bytes to the clear byte count of the first sub-sample.
|
||||
if (cryptoInfo.numBytesOfClearData == null) {
|
||||
cryptoInfo.numBytesOfClearData = new int[1];
|
||||
}
|
||||
cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes;
|
||||
return cryptoInfo;
|
||||
}
|
||||
|
||||
private boolean shouldWaitForKeys(boolean sampleEncrypted) throws ExoPlaybackException {
|
||||
if (!openedDrmSession) {
|
||||
return false;
|
||||
}
|
||||
int drmManagerState = drmSessionManager.getState();
|
||||
if (drmManagerState == DrmSessionManager.STATE_ERROR) {
|
||||
throw new ExoPlaybackException(drmSessionManager.getError());
|
||||
}
|
||||
if (drmManagerState != DrmSessionManager.STATE_OPENED_WITH_KEYS &&
|
||||
(sampleEncrypted || !playClearSamplesWithoutKeys)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a new format is read from the upstream {@link SampleSource}.
|
||||
*
|
||||
* @param formatHolder Holds the new format.
|
||||
* @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}.
|
||||
*/
|
||||
private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
|
||||
MediaFormat oldFormat = format;
|
||||
format = formatHolder.format;
|
||||
drmInitData = formatHolder.drmInitData;
|
||||
if (codec != null && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) {
|
||||
codecReconfigured = true;
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
||||
} else {
|
||||
releaseCodec();
|
||||
maybeInitCodec();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the output format of the {@link MediaCodec} changes.
|
||||
* <p>
|
||||
* The default implementation is a no-op.
|
||||
*
|
||||
* @param format The new output format.
|
||||
*/
|
||||
protected void onOutputFormatChanged(android.media.MediaFormat format) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the existing {@link MediaCodec} should be reconfigured for a new format by
|
||||
* sending codec specific initialization data at the start of the next input buffer. If true is
|
||||
* returned then the {@link MediaCodec} instance will be reconfigured in this way. If false is
|
||||
* returned then the instance will be released, and a new instance will be created for the new
|
||||
* format.
|
||||
* <p>
|
||||
* The default implementation returns false.
|
||||
*
|
||||
* @param codec The existing {@link MediaCodec} instance.
|
||||
* @param codecIsAdaptive Whether the codec is adaptive.
|
||||
* @param oldFormat The format for which the existing instance is configured.
|
||||
* @param newFormat The new format.
|
||||
* @return True if the existing instance can be reconfigured. False otherwise.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
|
||||
MediaFormat oldFormat, MediaFormat newFormat) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isEnded() {
|
||||
return outputStreamEnded;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isReady() {
|
||||
return format != null && !waitingForKeys
|
||||
&& ((codec == null && !shouldInitCodec()) // We don't want the codec
|
||||
|| outputIndex >= 0 // Or we have an output buffer ready to release
|
||||
|| inputIndex < 0 // Or we don't have any input buffers to write to
|
||||
|| isWithinHotswapPeriod()); // Or the codec is being hotswapped
|
||||
}
|
||||
|
||||
private boolean isWithinHotswapPeriod() {
|
||||
return SystemClock.elapsedRealtime() < codecHotswapTimeMs + MAX_CODEC_HOTSWAP_TIME_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if it may be possible to drain more output data. False otherwise.
|
||||
* @throws ExoPlaybackException If an error occurs draining the output buffer.
|
||||
*/
|
||||
private boolean drainOutputBuffer(long timeUs) throws ExoPlaybackException {
|
||||
if (outputStreamEnded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outputIndex < 0) {
|
||||
outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, 0);
|
||||
}
|
||||
|
||||
if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
onOutputFormatChanged(codec.getOutputFormat());
|
||||
codecCounters.outputFormatChangedCount++;
|
||||
return true;
|
||||
} else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
|
||||
outputBuffers = codec.getOutputBuffers();
|
||||
codecCounters.outputBuffersChangedCount++;
|
||||
return true;
|
||||
} else if (outputIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
||||
outputStreamEnded = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs)) {
|
||||
codec.releaseOutputBuffer(outputIndex, false);
|
||||
outputIndex = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo,
|
||||
outputIndex)) {
|
||||
currentPositionUs = outputBufferInfo.presentationTimeUs;
|
||||
outputIndex = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the provided output buffer.
|
||||
*
|
||||
* @return True if the output buffer was processed (e.g. rendered or discarded) and hence is no
|
||||
* longer required. False otherwise.
|
||||
* @throws ExoPlaybackException If an error occurs processing the output buffer.
|
||||
*/
|
||||
protected abstract boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
|
||||
MediaCodec.BufferInfo bufferInfo, int bufferIndex) throws ExoPlaybackException;
|
||||
|
||||
/**
|
||||
* Returns the name of the secure variant of a given decoder.
|
||||
*/
|
||||
private static String getSecureDecoderName(String rawDecoderName) {
|
||||
return rawDecoderName + ".secure";
|
||||
}
|
||||
|
||||
private void notifyDecoderInitializationError(final DecoderInitializationException e) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onDecoderInitializationError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyCryptoError(final CryptoException e) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onCryptoError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||
import android.media.MediaCodecInfo.CodecProfileLevel;
|
||||
import android.media.MediaCodecList;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* A utility class for querying the available codecs.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public class MediaCodecUtil {
|
||||
|
||||
private static final HashMap<String, Pair<MediaCodecInfo, CodecCapabilities>> codecs =
|
||||
new HashMap<String, Pair<MediaCodecInfo, CodecCapabilities>>();
|
||||
|
||||
/**
|
||||
* Get information about the decoder that will be used for a given mime type. If no decoder
|
||||
* exists for the mime type then null is returned.
|
||||
*
|
||||
* @param mimeType The mime type.
|
||||
* @return Information about the decoder that will be used, or null if no decoder exists.
|
||||
*/
|
||||
public static DecoderInfo getDecoderInfo(String mimeType) {
|
||||
Pair<MediaCodecInfo, CodecCapabilities> info = getMediaCodecInfo(mimeType);
|
||||
if (info == null) {
|
||||
return null;
|
||||
}
|
||||
return new DecoderInfo(info.first.getName(), isAdaptive(info.second));
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional call to warm the codec cache. Call from any appropriate
|
||||
* place to hide latency.
|
||||
*/
|
||||
public static synchronized void warmCodecs(String[] mimeTypes) {
|
||||
for (int i = 0; i < mimeTypes.length; i++) {
|
||||
getMediaCodecInfo(mimeTypes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the best decoder and its capabilities for the given mimeType. If there's no decoder
|
||||
* returns null.
|
||||
*/
|
||||
private static synchronized Pair<MediaCodecInfo, CodecCapabilities> getMediaCodecInfo(
|
||||
String mimeType) {
|
||||
Pair<MediaCodecInfo, CodecCapabilities> result = codecs.get(mimeType);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
int numberOfCodecs = MediaCodecList.getCodecCount();
|
||||
// Note: MediaCodecList is sorted by the framework such that the best decoders come first.
|
||||
for (int i = 0; i < numberOfCodecs; i++) {
|
||||
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
|
||||
String codecName = info.getName();
|
||||
if (!info.isEncoder() && isOmxCodec(codecName)) {
|
||||
String[] supportedTypes = info.getSupportedTypes();
|
||||
for (int j = 0; j < supportedTypes.length; j++) {
|
||||
String supportedType = supportedTypes[j];
|
||||
if (supportedType.equalsIgnoreCase(mimeType)) {
|
||||
result = Pair.create(info, info.getCapabilitiesForType(supportedType));
|
||||
codecs.put(mimeType, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isOmxCodec(String name) {
|
||||
return name.startsWith("OMX.");
|
||||
}
|
||||
|
||||
private static boolean isAdaptive(CodecCapabilities capabilities) {
|
||||
if (Util.SDK_INT >= 19) {
|
||||
return isAdaptiveV19(capabilities);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(19)
|
||||
private static boolean isAdaptiveV19(CodecCapabilities capabilities) {
|
||||
return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param profile An AVC profile constant from {@link CodecProfileLevel}.
|
||||
* @param level An AVC profile level from {@link CodecProfileLevel}.
|
||||
* @return Whether the specified profile is supported at the specified level.
|
||||
*/
|
||||
public static boolean isH264ProfileSupported(int profile, int level) {
|
||||
Pair<MediaCodecInfo, CodecCapabilities> info = getMediaCodecInfo(MimeTypes.VIDEO_H264);
|
||||
if (info == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CodecCapabilities capabilities = info.second;
|
||||
for (int i = 0; i < capabilities.profileLevels.length; i++) {
|
||||
CodecProfileLevel profileLevel = capabilities.profileLevels[i];
|
||||
if (profileLevel.profile == profile && profileLevel.level >= level) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum frame size for an H264 stream that can be decoded on the device.
|
||||
*/
|
||||
public static int maxH264DecodableFrameSize() {
|
||||
Pair<MediaCodecInfo, CodecCapabilities> info = getMediaCodecInfo(MimeTypes.VIDEO_H264);
|
||||
if (info == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int maxH264DecodableFrameSize = 0;
|
||||
CodecCapabilities capabilities = info.second;
|
||||
for (int i = 0; i < capabilities.profileLevels.length; i++) {
|
||||
CodecProfileLevel profileLevel = capabilities.profileLevels[i];
|
||||
maxH264DecodableFrameSize = Math.max(
|
||||
avcLevelToMaxFrameSize(profileLevel.level), maxH264DecodableFrameSize);
|
||||
}
|
||||
|
||||
return maxH264DecodableFrameSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conversion values taken from: https://en.wikipedia.org/wiki/H.264/MPEG-4_AVC.
|
||||
*
|
||||
* @param avcLevel one of CodecProfileLevel.AVCLevel* constants.
|
||||
* @return maximum frame size that can be decoded by a decoder with the specified avc level
|
||||
* (or {@code -1} if the level is not recognized)
|
||||
*/
|
||||
private static int avcLevelToMaxFrameSize(int avcLevel) {
|
||||
switch (avcLevel) {
|
||||
case CodecProfileLevel.AVCLevel1: return 25344;
|
||||
case CodecProfileLevel.AVCLevel1b: return 25344;
|
||||
case CodecProfileLevel.AVCLevel12: return 101376;
|
||||
case CodecProfileLevel.AVCLevel13: return 101376;
|
||||
case CodecProfileLevel.AVCLevel2: return 101376;
|
||||
case CodecProfileLevel.AVCLevel21: return 202752;
|
||||
case CodecProfileLevel.AVCLevel22: return 414720;
|
||||
case CodecProfileLevel.AVCLevel3: return 414720;
|
||||
case CodecProfileLevel.AVCLevel31: return 921600;
|
||||
case CodecProfileLevel.AVCLevel32: return 1310720;
|
||||
case CodecProfileLevel.AVCLevel4: return 2097152;
|
||||
case CodecProfileLevel.AVCLevel41: return 2097152;
|
||||
case CodecProfileLevel.AVCLevel42: return 2228224;
|
||||
case CodecProfileLevel.AVCLevel5: return 5652480;
|
||||
case CodecProfileLevel.AVCLevel51: return 9437184;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,439 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
import com.google.android.exoplayer.util.TraceUtil;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCrypto;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Decodes and renders video using {@MediaCodec}.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be notified of {@link MediaCodecVideoTrackRenderer}
|
||||
* events.
|
||||
*/
|
||||
public interface EventListener extends MediaCodecTrackRenderer.EventListener {
|
||||
|
||||
/**
|
||||
* Invoked to report the number of frames dropped by the renderer. Dropped frames are reported
|
||||
* whenever the renderer is stopped having dropped frames, and optionally, whenever the count
|
||||
* reaches a specified threshold whilst the renderer is started.
|
||||
*
|
||||
* @param count The number of dropped frames.
|
||||
* @param elapsed The duration in milliseconds over which the frames were dropped. This
|
||||
* duration is timed from when the renderer was started or from when dropped frames were
|
||||
* last reported (whichever was more recent), and not from when the first of the reported
|
||||
* drops occurred.
|
||||
*/
|
||||
void onDroppedFrames(int count, long elapsed);
|
||||
|
||||
/**
|
||||
* Invoked each time there's a change in the size of the video being rendered.
|
||||
*
|
||||
* @param width The video width in pixels.
|
||||
* @param height The video height in pixels.
|
||||
*/
|
||||
void onVideoSizeChanged(int width, int height);
|
||||
|
||||
/**
|
||||
* Invoked when a frame is rendered to a surface for the first time following that surface
|
||||
* having been set as the target for the renderer.
|
||||
*
|
||||
* @param surface The surface to which a first frame has been rendered.
|
||||
*/
|
||||
void onDrawnToSurface(Surface surface);
|
||||
|
||||
}
|
||||
|
||||
// TODO: Use MediaFormat constants if these get exposed through the API. See [redacted].
|
||||
private static final String KEY_CROP_LEFT = "crop-left";
|
||||
private static final String KEY_CROP_RIGHT = "crop-right";
|
||||
private static final String KEY_CROP_BOTTOM = "crop-bottom";
|
||||
private static final String KEY_CROP_TOP = "crop-top";
|
||||
|
||||
/**
|
||||
* The type of a message that can be passed to an instance of this class via
|
||||
* {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object
|
||||
* should be the target {@link Surface}, or null.
|
||||
*/
|
||||
public static final int MSG_SET_SURFACE = 1;
|
||||
|
||||
private final EventListener eventListener;
|
||||
private final long allowedJoiningTimeUs;
|
||||
private final int videoScalingMode;
|
||||
private final int maxDroppedFrameCountToNotify;
|
||||
|
||||
private Surface surface;
|
||||
private boolean drawnToSurface;
|
||||
private boolean renderedFirstFrame;
|
||||
private long joiningDeadlineUs;
|
||||
private long droppedFrameAccumulationStartTimeMs;
|
||||
private int droppedFrameCount;
|
||||
|
||||
private int currentWidth;
|
||||
private int currentHeight;
|
||||
private int lastReportedWidth;
|
||||
private int lastReportedHeight;
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
* @param videoScalingMode The scaling mode to pass to
|
||||
* {@link MediaCodec#setVideoScalingMode(int)}.
|
||||
*/
|
||||
public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode) {
|
||||
this(source, null, true, videoScalingMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
|
||||
* content is not required.
|
||||
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
|
||||
* For example a media file may start with a short clear region so as to allow playback to
|
||||
* begin in parallel with key acquisision. This parameter specifies whether the renderer is
|
||||
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||
* @param videoScalingMode The scaling mode to pass to
|
||||
* {@link MediaCodec#setVideoScalingMode(int)}.
|
||||
*/
|
||||
public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys, int videoScalingMode) {
|
||||
this(source, drmSessionManager, playClearSamplesWithoutKeys, videoScalingMode, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
* @param videoScalingMode The scaling mode to pass to
|
||||
* {@link MediaCodec#setVideoScalingMode(int)}.
|
||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||
* can attempt to seamlessly join an ongoing playback.
|
||||
*/
|
||||
public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode,
|
||||
long allowedJoiningTimeMs) {
|
||||
this(source, null, true, videoScalingMode, allowedJoiningTimeMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
|
||||
* content is not required.
|
||||
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
|
||||
* For example a media file may start with a short clear region so as to allow playback to
|
||||
* begin in parallel with key acquisision. This parameter specifies whether the renderer is
|
||||
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||
* @param videoScalingMode The scaling mode to pass to
|
||||
* {@link MediaCodec#setVideoScalingMode(int)}.
|
||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||
* can attempt to seamlessly join an ongoing playback.
|
||||
*/
|
||||
public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys, int videoScalingMode, long allowedJoiningTimeMs) {
|
||||
this(source, drmSessionManager, playClearSamplesWithoutKeys, videoScalingMode,
|
||||
allowedJoiningTimeMs, null, null, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
* @param videoScalingMode The scaling mode to pass to
|
||||
* {@link MediaCodec#setVideoScalingMode(int)}.
|
||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||
* can attempt to seamlessly join an ongoing playback.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between
|
||||
* invocations of {@link EventListener#onDroppedFrames(int, long)}.
|
||||
*/
|
||||
public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode,
|
||||
long allowedJoiningTimeMs, Handler eventHandler, EventListener eventListener,
|
||||
int maxDroppedFrameCountToNotify) {
|
||||
this(source, null, true, videoScalingMode, allowedJoiningTimeMs, eventHandler, eventListener,
|
||||
maxDroppedFrameCountToNotify);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source The upstream source from which the renderer obtains samples.
|
||||
* @param drmSessionManager For use with encrypted content. May be null if support for encrypted
|
||||
* content is not required.
|
||||
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
|
||||
* For example a media file may start with a short clear region so as to allow playback to
|
||||
* begin in parallel with key acquisision. This parameter specifies whether the renderer is
|
||||
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||
* @param videoScalingMode The scaling mode to pass to
|
||||
* {@link MediaCodec#setVideoScalingMode(int)}.
|
||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||
* can attempt to seamlessly join an ongoing playback.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between
|
||||
* invocations of {@link EventListener#onDroppedFrames(int, long)}.
|
||||
*/
|
||||
public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys, int videoScalingMode, long allowedJoiningTimeMs,
|
||||
Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) {
|
||||
super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener);
|
||||
this.videoScalingMode = videoScalingMode;
|
||||
this.allowedJoiningTimeUs = allowedJoiningTimeMs * 1000;
|
||||
this.eventListener = eventListener;
|
||||
this.maxDroppedFrameCountToNotify = maxDroppedFrameCountToNotify;
|
||||
joiningDeadlineUs = -1;
|
||||
currentWidth = -1;
|
||||
currentHeight = -1;
|
||||
lastReportedWidth = -1;
|
||||
lastReportedHeight = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean handlesMimeType(String mimeType) {
|
||||
return MimeTypes.isVideo(mimeType) && super.handlesMimeType(mimeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnabled(long startTimeUs, boolean joining) {
|
||||
super.onEnabled(startTimeUs, joining);
|
||||
renderedFirstFrame = false;
|
||||
if (joining && allowedJoiningTimeUs > 0) {
|
||||
joiningDeadlineUs = SystemClock.elapsedRealtime() * 1000L + allowedJoiningTimeUs;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seekTo(long timeUs) throws ExoPlaybackException {
|
||||
super.seekTo(timeUs);
|
||||
renderedFirstFrame = false;
|
||||
joiningDeadlineUs = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isReady() {
|
||||
if (super.isReady() && (renderedFirstFrame || !codecInitialized())) {
|
||||
// Ready. If we were joining then we've now joined, so clear the joining deadline.
|
||||
joiningDeadlineUs = -1;
|
||||
return true;
|
||||
} else if (joiningDeadlineUs == -1) {
|
||||
// Not joining.
|
||||
return false;
|
||||
} else if (SystemClock.elapsedRealtime() * 1000 < joiningDeadlineUs) {
|
||||
// Joining and still within the joining deadline.
|
||||
return true;
|
||||
} else {
|
||||
// The joining deadline has been exceeded. Give up and clear the deadline.
|
||||
joiningDeadlineUs = -1;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStarted() {
|
||||
super.onStarted();
|
||||
droppedFrameCount = 0;
|
||||
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStopped() {
|
||||
super.onStopped();
|
||||
joiningDeadlineUs = -1;
|
||||
notifyAndResetDroppedFrameCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisabled() {
|
||||
super.onDisabled();
|
||||
currentWidth = -1;
|
||||
currentHeight = -1;
|
||||
lastReportedWidth = -1;
|
||||
lastReportedHeight = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
|
||||
if (messageType == MSG_SET_SURFACE) {
|
||||
setSurface((Surface) message);
|
||||
} else {
|
||||
super.handleMessage(messageType, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param surface The surface to set.
|
||||
* @throws ExoPlaybackException
|
||||
*/
|
||||
private void setSurface(Surface surface) throws ExoPlaybackException {
|
||||
if (this.surface == surface) {
|
||||
return;
|
||||
}
|
||||
this.surface = surface;
|
||||
this.drawnToSurface = false;
|
||||
int state = getState();
|
||||
if (state == TrackRenderer.STATE_ENABLED || state == TrackRenderer.STATE_STARTED) {
|
||||
releaseCodec();
|
||||
maybeInitCodec();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldInitCodec() {
|
||||
return super.shouldInitCodec() && surface != null;
|
||||
}
|
||||
|
||||
// Override configureCodec to provide the surface.
|
||||
@Override
|
||||
protected void configureCodec(MediaCodec codec, android.media.MediaFormat format,
|
||||
MediaCrypto crypto) {
|
||||
codec.configure(format, surface, crypto, 0);
|
||||
codec.setVideoScalingMode(videoScalingMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onOutputFormatChanged(android.media.MediaFormat format) {
|
||||
boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT)
|
||||
&& format.containsKey(KEY_CROP_BOTTOM) && format.containsKey(KEY_CROP_TOP);
|
||||
currentWidth = hasCrop
|
||||
? format.getInteger(KEY_CROP_RIGHT) - format.getInteger(KEY_CROP_LEFT) + 1
|
||||
: format.getInteger(android.media.MediaFormat.KEY_WIDTH);
|
||||
currentHeight = hasCrop
|
||||
? format.getInteger(KEY_CROP_BOTTOM) - format.getInteger(KEY_CROP_TOP) + 1
|
||||
: format.getInteger(android.media.MediaFormat.KEY_HEIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
|
||||
MediaFormat oldFormat, MediaFormat newFormat) {
|
||||
// TODO: Relax this check to also allow non-H264 adaptive decoders.
|
||||
return newFormat.mimeType.equals(MimeTypes.VIDEO_H264)
|
||||
&& oldFormat.mimeType.equals(MimeTypes.VIDEO_H264)
|
||||
&& codecIsAdaptive
|
||||
|| (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
|
||||
MediaCodec.BufferInfo bufferInfo, int bufferIndex) {
|
||||
long earlyUs = bufferInfo.presentationTimeUs - timeUs;
|
||||
if (earlyUs < -30000) {
|
||||
// We're more than 30ms late rendering the frame.
|
||||
dropOutputBuffer(codec, bufferIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!renderedFirstFrame) {
|
||||
renderOutputBuffer(codec, bufferIndex);
|
||||
renderedFirstFrame = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (getState() == TrackRenderer.STATE_STARTED && earlyUs < 30000) {
|
||||
if (earlyUs > 11000) {
|
||||
// We're a little too early to render the frame. Sleep until the frame can be rendered.
|
||||
// Note: The 11ms threshold was chosen fairly arbitrarily.
|
||||
try {
|
||||
// Subtracting 10000 rather than 11000 ensures that the sleep time will be at least 1ms.
|
||||
Thread.sleep((earlyUs - 10000) / 1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
renderOutputBuffer(codec, bufferIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
// We're either not playing, or it's not time to render the frame yet.
|
||||
return false;
|
||||
}
|
||||
|
||||
private void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
|
||||
TraceUtil.beginSection("dropVideoBuffer");
|
||||
codec.releaseOutputBuffer(bufferIndex, false);
|
||||
TraceUtil.endSection();
|
||||
codecCounters.droppedOutputBufferCount++;
|
||||
droppedFrameCount++;
|
||||
if (droppedFrameCount == maxDroppedFrameCountToNotify) {
|
||||
notifyAndResetDroppedFrameCount();
|
||||
}
|
||||
}
|
||||
|
||||
private void renderOutputBuffer(MediaCodec codec, int bufferIndex) {
|
||||
if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight) {
|
||||
lastReportedWidth = currentWidth;
|
||||
lastReportedHeight = currentHeight;
|
||||
notifyVideoSizeChanged(currentWidth, currentHeight);
|
||||
}
|
||||
TraceUtil.beginSection("renderVideoBuffer");
|
||||
codec.releaseOutputBuffer(bufferIndex, true);
|
||||
TraceUtil.endSection();
|
||||
codecCounters.renderedOutputBufferCount++;
|
||||
if (!drawnToSurface) {
|
||||
drawnToSurface = true;
|
||||
notifyDrawnToSurface(surface);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyVideoSizeChanged(final int width, final int height) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onVideoSizeChanged(width, height);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyDrawnToSurface(final Surface surface) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onDrawnToSurface(surface);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyAndResetDroppedFrameCount() {
|
||||
if (eventHandler != null && eventListener != null && droppedFrameCount > 0) {
|
||||
long now = SystemClock.elapsedRealtime();
|
||||
final int countToNotify = droppedFrameCount;
|
||||
final long elapsedToNotify = now - droppedFrameAccumulationStartTimeMs;
|
||||
droppedFrameCount = 0;
|
||||
droppedFrameAccumulationStartTimeMs = now;
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onDroppedFrames(countToNotify, elapsedToNotify);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Encapsulates the information describing the format of media data, be it audio or video.
|
||||
*/
|
||||
public class MediaFormat {
|
||||
|
||||
public static final int NO_VALUE = -1;
|
||||
|
||||
public final String mimeType;
|
||||
public final int maxInputSize;
|
||||
|
||||
public final int width;
|
||||
public final int height;
|
||||
|
||||
public final int channelCount;
|
||||
public final int sampleRate;
|
||||
|
||||
private int maxWidth;
|
||||
private int maxHeight;
|
||||
|
||||
public final List<byte[]> initializationData;
|
||||
|
||||
// Lazy-initialized hashcode.
|
||||
private int hashCode;
|
||||
// Possibly-lazy-initialized framework media format.
|
||||
private android.media.MediaFormat frameworkMediaFormat;
|
||||
|
||||
@TargetApi(16)
|
||||
public static MediaFormat createFromFrameworkMediaFormatV16(android.media.MediaFormat format) {
|
||||
return new MediaFormat(format);
|
||||
}
|
||||
|
||||
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
|
||||
int height, List<byte[]> initializationData) {
|
||||
return new MediaFormat(mimeType, maxInputSize, width, height, NO_VALUE, NO_VALUE,
|
||||
initializationData);
|
||||
}
|
||||
|
||||
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
|
||||
int sampleRate, List<byte[]> initializationData) {
|
||||
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, channelCount, sampleRate,
|
||||
initializationData);
|
||||
}
|
||||
|
||||
@TargetApi(16)
|
||||
private MediaFormat(android.media.MediaFormat format) {
|
||||
this.frameworkMediaFormat = format;
|
||||
mimeType = format.getString(android.media.MediaFormat.KEY_MIME);
|
||||
maxInputSize = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE);
|
||||
width = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_WIDTH);
|
||||
height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT);
|
||||
channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT);
|
||||
sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE);
|
||||
initializationData = new ArrayList<byte[]>();
|
||||
for (int i = 0; format.containsKey("csd-" + i); i++) {
|
||||
ByteBuffer buffer = format.getByteBuffer("csd-" + i);
|
||||
byte[] data = new byte[buffer.limit()];
|
||||
buffer.get(data);
|
||||
initializationData.add(data);
|
||||
buffer.flip();
|
||||
}
|
||||
maxWidth = NO_VALUE;
|
||||
maxHeight = NO_VALUE;
|
||||
}
|
||||
|
||||
private MediaFormat(String mimeType, int maxInputSize, int width, int height, int channelCount,
|
||||
int sampleRate, List<byte[]> initializationData) {
|
||||
this.mimeType = mimeType;
|
||||
this.maxInputSize = maxInputSize;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.channelCount = channelCount;
|
||||
this.sampleRate = sampleRate;
|
||||
this.initializationData = initializationData == null ? Collections.<byte[]>emptyList()
|
||||
: initializationData;
|
||||
maxWidth = NO_VALUE;
|
||||
maxHeight = NO_VALUE;
|
||||
}
|
||||
|
||||
public void setMaxVideoDimensions(int maxWidth, int maxHeight) {
|
||||
this.maxWidth = maxWidth;
|
||||
this.maxHeight = maxHeight;
|
||||
if (frameworkMediaFormat != null) {
|
||||
maybeSetMaxDimensionsV16(frameworkMediaFormat);
|
||||
}
|
||||
}
|
||||
|
||||
public int getMaxVideoWidth() {
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
public int getMaxVideoHeight() {
|
||||
return maxHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (hashCode == 0) {
|
||||
int result = 17;
|
||||
result = 31 * result + mimeType == null ? 0 : mimeType.hashCode();
|
||||
result = 31 * result + maxInputSize;
|
||||
result = 31 * result + width;
|
||||
result = 31 * result + height;
|
||||
result = 31 * result + maxWidth;
|
||||
result = 31 * result + maxHeight;
|
||||
result = 31 * result + channelCount;
|
||||
result = 31 * result + sampleRate;
|
||||
for (int i = 0; i < initializationData.size(); i++) {
|
||||
result = 31 * result + Arrays.hashCode(initializationData.get(i));
|
||||
}
|
||||
hashCode = result;
|
||||
}
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
MediaFormat other = (MediaFormat) obj;
|
||||
if (maxInputSize != other.maxInputSize || width != other.width || height != other.height ||
|
||||
maxWidth != other.maxWidth || maxHeight != other.maxHeight ||
|
||||
channelCount != other.channelCount || sampleRate != other.sampleRate ||
|
||||
!Util.areEqual(mimeType, other.mimeType) ||
|
||||
initializationData.size() != other.initializationData.size()) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < initializationData.size(); i++) {
|
||||
if (!Arrays.equals(initializationData.get(i), other.initializationData.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", " +
|
||||
channelCount + ", " + sampleRate + ", " + maxWidth + ", " + maxHeight + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A {@link MediaFormat} representation of this format.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public final android.media.MediaFormat getFrameworkMediaFormatV16() {
|
||||
if (frameworkMediaFormat == null) {
|
||||
android.media.MediaFormat format = new android.media.MediaFormat();
|
||||
format.setString(android.media.MediaFormat.KEY_MIME, mimeType);
|
||||
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
|
||||
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_WIDTH, width);
|
||||
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT, height);
|
||||
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount);
|
||||
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE, sampleRate);
|
||||
for (int i = 0; i < initializationData.size(); i++) {
|
||||
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
|
||||
}
|
||||
maybeSetMaxDimensionsV16(format);
|
||||
frameworkMediaFormat = format;
|
||||
}
|
||||
return frameworkMediaFormat;
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
@TargetApi(16)
|
||||
private final void maybeSetMaxDimensionsV16(android.media.MediaFormat format) {
|
||||
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_WIDTH, maxWidth);
|
||||
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_HEIGHT, maxHeight);
|
||||
}
|
||||
|
||||
@TargetApi(16)
|
||||
private static final void maybeSetIntegerV16(android.media.MediaFormat format, String key,
|
||||
int value) {
|
||||
if (value != NO_VALUE) {
|
||||
format.setInteger(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(16)
|
||||
private static final int getOptionalIntegerV16(android.media.MediaFormat format,
|
||||
String key) {
|
||||
return format.containsKey(key) ? format.getInteger(key) : NO_VALUE;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Thrown when an error occurs parsing media data.
|
||||
*/
|
||||
public class ParserException extends IOException {
|
||||
|
||||
public ParserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ParserException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Holds sample data and corresponding metadata.
|
||||
*/
|
||||
public final class SampleHolder {
|
||||
|
||||
/**
|
||||
* Whether a {@link SampleSource} is permitted to replace {@link #data} if its current value is
|
||||
* null or of insufficient size to hold the sample.
|
||||
*/
|
||||
public final boolean allowDataBufferReplacement;
|
||||
|
||||
public final CryptoInfo cryptoInfo;
|
||||
|
||||
/**
|
||||
* A buffer holding the sample data.
|
||||
*/
|
||||
public ByteBuffer data;
|
||||
|
||||
/**
|
||||
* The size of the sample in bytes.
|
||||
*/
|
||||
public int size;
|
||||
|
||||
/**
|
||||
* Flags that accompany the sample. A combination of
|
||||
* {@link android.media.MediaExtractor#SAMPLE_FLAG_SYNC} and
|
||||
* {@link android.media.MediaExtractor#SAMPLE_FLAG_ENCRYPTED}
|
||||
*/
|
||||
public int flags;
|
||||
|
||||
/**
|
||||
* The time at which the sample should be presented.
|
||||
*/
|
||||
public long timeUs;
|
||||
|
||||
/**
|
||||
* If true then the sample should be decoded, but should not be presented.
|
||||
*/
|
||||
public boolean decodeOnly;
|
||||
|
||||
/**
|
||||
* @param allowDataBufferReplacement See {@link #allowDataBufferReplacement}.
|
||||
*/
|
||||
public SampleHolder(boolean allowDataBufferReplacement) {
|
||||
this.cryptoInfo = new CryptoInfo();
|
||||
this.allowDataBufferReplacement = allowDataBufferReplacement;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A source of media samples.
|
||||
* <p>
|
||||
* A {@link SampleSource} may expose one or multiple tracks. The number of tracks and information
|
||||
* about each can be queried using {@link #getTrackCount()} and {@link #getTrackInfo(int)}
|
||||
* respectively.
|
||||
*/
|
||||
public interface SampleSource {
|
||||
|
||||
/**
|
||||
* The end of stream has been reached.
|
||||
*/
|
||||
public static final int END_OF_STREAM = -1;
|
||||
/**
|
||||
* Neither a sample nor a format was read in full. This may be because insufficient data is
|
||||
* buffered upstream. If multiple tracks are enabled, this return value may indicate that the
|
||||
* next piece of data to be returned from the {@link SampleSource} corresponds to a different
|
||||
* track than the one for which data was requested.
|
||||
*/
|
||||
public static final int NOTHING_READ = -2;
|
||||
/**
|
||||
* A sample was read.
|
||||
*/
|
||||
public static final int SAMPLE_READ = -3;
|
||||
/**
|
||||
* A format was read.
|
||||
*/
|
||||
public static final int FORMAT_READ = -4;
|
||||
/**
|
||||
* A discontinuity in the sample stream.
|
||||
*/
|
||||
public static final int DISCONTINUITY_READ = -5;
|
||||
|
||||
/**
|
||||
* Prepares the source.
|
||||
* <p>
|
||||
* Preparation may require reading from the data source (e.g. to determine the available tracks
|
||||
* and formats). If insufficient data is available then the call will return rather than block.
|
||||
* The method can be called repeatedly until the return value indicates success.
|
||||
*
|
||||
* @return True if the source was prepared successfully, false otherwise.
|
||||
* @throws IOException If an error occurred preparing the source.
|
||||
*/
|
||||
public boolean prepare() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns the number of tracks exposed by the source.
|
||||
*
|
||||
* @return The number of tracks.
|
||||
*/
|
||||
public int getTrackCount();
|
||||
|
||||
/**
|
||||
* Returns information about the specified track.
|
||||
* <p>
|
||||
* This method should not be called until after the source has been successfully prepared.
|
||||
*
|
||||
* @return Information about the specified track.
|
||||
*/
|
||||
public TrackInfo getTrackInfo(int track);
|
||||
|
||||
/**
|
||||
* Enable the specified track. This allows the track's format and samples to be read from
|
||||
* {@link #readData(int, long, FormatHolder, SampleHolder, boolean)}.
|
||||
* <p>
|
||||
* This method should not be called until after the source has been successfully prepared.
|
||||
*
|
||||
* @param track The track to enable.
|
||||
* @param timeUs The player's current playback position.
|
||||
*/
|
||||
public void enable(int track, long timeUs);
|
||||
|
||||
/**
|
||||
* Disable the specified track.
|
||||
* <p>
|
||||
* This method should not be called until after the source has been successfully prepared.
|
||||
*
|
||||
* @param track The track to disable.
|
||||
*/
|
||||
public void disable(int track);
|
||||
|
||||
/**
|
||||
* Indicates to the source that it should still be buffering data.
|
||||
*
|
||||
* @param playbackPositionUs The current playback position.
|
||||
*/
|
||||
public void continueBuffering(long playbackPositionUs);
|
||||
|
||||
/**
|
||||
* Attempts to read either a sample, a new format or or a discontinuity from the source.
|
||||
* <p>
|
||||
* This method should not be called until after the source has been successfully prepared.
|
||||
* <p>
|
||||
* Note that where multiple tracks are enabled, {@link #NOTHING_READ} may be returned if the
|
||||
* next piece of data to be read from the {@link SampleSource} corresponds to a different track
|
||||
* than the one for which data was requested.
|
||||
*
|
||||
* @param track The track from which to read.
|
||||
* @param playbackPositionUs The current playback position.
|
||||
* @param formatHolder A {@link FormatHolder} object to populate in the case of a new format.
|
||||
* @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. If
|
||||
* the caller requires the sample data then it must ensure that {@link SampleHolder#data}
|
||||
* references a valid output buffer.
|
||||
* @param onlyReadDiscontinuity Whether to only read a discontinuity. If true, only
|
||||
* {@link #DISCONTINUITY_READ} or {@link #NOTHING_READ} can be returned.
|
||||
* @return The result, which can be {@link #SAMPLE_READ}, {@link #FORMAT_READ},
|
||||
* {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}.
|
||||
* @throws IOException If an error occurred reading from the source.
|
||||
*/
|
||||
public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
|
||||
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException;
|
||||
|
||||
/**
|
||||
* Seeks to the specified time in microseconds.
|
||||
* <p>
|
||||
* This method should not be called until after the source has been successfully prepared.
|
||||
*
|
||||
* @param timeUs The seek position in microseconds.
|
||||
*/
|
||||
public void seekToUs(long timeUs);
|
||||
|
||||
/**
|
||||
* Returns an estimate of the position up to which data is buffered.
|
||||
* <p>
|
||||
* This method should not be called until after the source has been successfully prepared.
|
||||
*
|
||||
* @return An estimate of the absolute position in micro-seconds up to which data is buffered,
|
||||
* or {@link TrackRenderer#END_OF_TRACK} if data is buffered to the end of the stream, or
|
||||
* {@link TrackRenderer#UNKNOWN_TIME} if no estimate is available.
|
||||
*/
|
||||
public long getBufferedPositionUs();
|
||||
|
||||
/**
|
||||
* Releases the {@link SampleSource}.
|
||||
*/
|
||||
public void release();
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Holds high level information about a media track.
|
||||
*/
|
||||
public final class TrackInfo {
|
||||
|
||||
public final String mimeType;
|
||||
public final long durationUs;
|
||||
|
||||
public TrackInfo(String mimeType, long durationUs) {
|
||||
this.mimeType = mimeType;
|
||||
this.durationUs = durationUs;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,348 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
/**
|
||||
* Renders a single component of media.
|
||||
*
|
||||
* <p>Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The player
|
||||
* will transition its renderers through various states as the overall playback state changes. The
|
||||
* valid state transitions are shown below, annotated with the methods that are invoked during each
|
||||
* transition.
|
||||
* <p align="center"><img src="../../../../../doc_src/images/trackrenderer_state.png"
|
||||
* alt="TrackRenderer state transitions"
|
||||
* border="0"/></p>
|
||||
*/
|
||||
public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||
|
||||
/**
|
||||
* The renderer has been released and should not be used.
|
||||
*/
|
||||
protected static final int STATE_RELEASED = -2;
|
||||
/**
|
||||
* The renderer should be ignored by the player.
|
||||
*/
|
||||
protected static final int STATE_IGNORE = -1;
|
||||
/**
|
||||
* The renderer has not yet been prepared.
|
||||
*/
|
||||
protected static final int STATE_UNPREPARED = 0;
|
||||
/**
|
||||
* The renderer has completed necessary preparation. Preparation may include, for example,
|
||||
* reading the header of a media file to determine the track format and duration.
|
||||
* <p>
|
||||
* The renderer should not hold scarce or expensive system resources (e.g. media decoders) and
|
||||
* should not be actively buffering media data when in this state.
|
||||
*/
|
||||
protected static final int STATE_PREPARED = 1;
|
||||
/**
|
||||
* The renderer is enabled. It should either be ready to be started, or be actively working
|
||||
* towards this state (e.g. a renderer in this state will typically hold any resources that it
|
||||
* requires, such as media decoders, and will have buffered or be buffering any media data that
|
||||
* is required to start playback).
|
||||
*/
|
||||
protected static final int STATE_ENABLED = 2;
|
||||
/**
|
||||
* The renderer is started. Calls to {@link #doSomeWork(long)} should cause the media to be
|
||||
* rendered.
|
||||
*/
|
||||
protected static final int STATE_STARTED = 3;
|
||||
|
||||
/**
|
||||
* Represents an unknown time or duration.
|
||||
*/
|
||||
public static final long UNKNOWN_TIME = -1;
|
||||
/**
|
||||
* Represents a time or duration that should match the duration of the longest track whose
|
||||
* duration is known.
|
||||
*/
|
||||
public static final long MATCH_LONGEST = -2;
|
||||
/**
|
||||
* Represents the time of the end of the track.
|
||||
*/
|
||||
public static final long END_OF_TRACK = -3;
|
||||
|
||||
private int state;
|
||||
|
||||
/**
|
||||
* A time source renderer is a renderer that, when started, advances its own playback position.
|
||||
* This means that {@link #getCurrentPositionUs()} will return increasing positions independently
|
||||
* to increasing values being passed to {@link #doSomeWork(long)}. A player may have at most one
|
||||
* time source renderer. If provided, the player will use such a renderer as its source of time
|
||||
* during playback.
|
||||
* <p>
|
||||
* This method may be called when the renderer is in any state.
|
||||
*
|
||||
* @return True if the renderer should be considered a time source. False otherwise.
|
||||
*/
|
||||
protected boolean isTimeSource() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state of the renderer.
|
||||
*
|
||||
* @return The current state (one of the STATE_* constants).
|
||||
*/
|
||||
protected final int getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the renderer. This method is non-blocking, and hence it may be necessary to call it
|
||||
* more than once in order to transition the renderer into the prepared state.
|
||||
*
|
||||
* @return The current state (one of the STATE_* constants), for convenience.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
/* package */ final int prepare() throws ExoPlaybackException {
|
||||
Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED);
|
||||
state = doPrepare();
|
||||
Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED ||
|
||||
state == TrackRenderer.STATE_PREPARED ||
|
||||
state == TrackRenderer.STATE_IGNORE);
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to make progress when the renderer is in the {@link #STATE_UNPREPARED} state. This
|
||||
* method will be called repeatedly until a value other than {@link #STATE_UNPREPARED} is
|
||||
* returned.
|
||||
* <p>
|
||||
* This method should return quickly, and should not block if the renderer is currently unable to
|
||||
* make any useful progress.
|
||||
*
|
||||
* @return The new state of the renderer. One of {@link #STATE_UNPREPARED},
|
||||
* {@link #STATE_PREPARED} and {@link #STATE_IGNORE}.
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
protected abstract int doPrepare() throws ExoPlaybackException;
|
||||
|
||||
/**
|
||||
* Enable the renderer.
|
||||
*
|
||||
* @param timeUs The player's current position.
|
||||
* @param joining Whether this renderer is being enabled to join an ongoing playback. If true
|
||||
* then {@link #start} must be called immediately after this method returns (unless a
|
||||
* {@link ExoPlaybackException} is thrown).
|
||||
*/
|
||||
/* package */ final void enable(long timeUs, boolean joining) throws ExoPlaybackException {
|
||||
Assertions.checkState(state == TrackRenderer.STATE_PREPARED);
|
||||
state = TrackRenderer.STATE_ENABLED;
|
||||
onEnabled(timeUs, joining);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the renderer is enabled.
|
||||
* <p>
|
||||
* The default implementation is a no-op.
|
||||
*
|
||||
* @param timeUs The player's current position.
|
||||
* @param joining Whether this renderer is being enabled to join an ongoing playback. If true
|
||||
* then {@link #onStarted} is guaranteed to be called immediately after this method returns
|
||||
* (unless a {@link ExoPlaybackException} is thrown).
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
protected void onEnabled(long timeUs, boolean joining) throws ExoPlaybackException {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the renderer, meaning that calls to {@link #doSomeWork(long)} will cause the
|
||||
* track to be rendered.
|
||||
*/
|
||||
/* package */ final void start() throws ExoPlaybackException {
|
||||
Assertions.checkState(state == TrackRenderer.STATE_ENABLED);
|
||||
state = TrackRenderer.STATE_STARTED;
|
||||
onStarted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the renderer is started.
|
||||
* <p>
|
||||
* The default implementation is a no-op.
|
||||
*
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
protected void onStarted() throws ExoPlaybackException {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the renderer.
|
||||
*/
|
||||
/* package */ final void stop() throws ExoPlaybackException {
|
||||
Assertions.checkState(state == TrackRenderer.STATE_STARTED);
|
||||
state = TrackRenderer.STATE_ENABLED;
|
||||
onStopped();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the renderer is stopped.
|
||||
* <p>
|
||||
* The default implementation is a no-op.
|
||||
*
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
protected void onStopped() throws ExoPlaybackException {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the renderer.
|
||||
*/
|
||||
/* package */ final void disable() throws ExoPlaybackException {
|
||||
Assertions.checkState(state == TrackRenderer.STATE_ENABLED);
|
||||
state = TrackRenderer.STATE_PREPARED;
|
||||
onDisabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the renderer is disabled.
|
||||
* <p>
|
||||
* The default implementation is a no-op.
|
||||
*
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
protected void onDisabled() throws ExoPlaybackException {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the renderer.
|
||||
*/
|
||||
/* package */ final void release() throws ExoPlaybackException {
|
||||
Assertions.checkState(state != TrackRenderer.STATE_ENABLED
|
||||
&& state != TrackRenderer.STATE_STARTED
|
||||
&& state != TrackRenderer.STATE_RELEASED);
|
||||
state = TrackRenderer.STATE_RELEASED;
|
||||
onReleased();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the renderer is released.
|
||||
* <p>
|
||||
* The default implementation is a no-op.
|
||||
*
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
protected void onReleased() throws ExoPlaybackException {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the renderer is ready for the {@link ExoPlayer} instance to transition to
|
||||
* {@link ExoPlayer#STATE_ENDED}. The player will make this transition as soon as {@code true} is
|
||||
* returned by all of its {@link TrackRenderer}s.
|
||||
* <p>
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
||||
*
|
||||
* @return Whether the renderer is ready for the player to transition to the ended state.
|
||||
*/
|
||||
protected abstract boolean isEnded();
|
||||
|
||||
/**
|
||||
* Whether the renderer is able to immediately render media from the current position.
|
||||
* <p>
|
||||
* If the renderer is in the {@link #STATE_STARTED} state then returning true indicates that the
|
||||
* renderer has everything that it needs to continue playback. Returning false indicates that
|
||||
* the player should pause until the renderer is ready.
|
||||
* <p>
|
||||
* If the renderer is in the {@link #STATE_ENABLED} state then returning true indicates that the
|
||||
* renderer is ready for playback to be started. Returning false indicates that it is not.
|
||||
* <p>
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
||||
*
|
||||
* @return True if the renderer is ready to render media. False otherwise.
|
||||
*/
|
||||
protected abstract boolean isReady();
|
||||
|
||||
/**
|
||||
* Invoked to make progress when the renderer is in the {@link #STATE_ENABLED} or
|
||||
* {@link #STATE_STARTED} states.
|
||||
* <p>
|
||||
* If the renderer's state is {@link #STATE_STARTED}, then repeated calls to this method should
|
||||
* cause the media track to be rendered. If the state is {@link #STATE_ENABLED}, then repeated
|
||||
* calls should make progress towards getting the renderer into a position where it is ready to
|
||||
* render the track.
|
||||
* <p>
|
||||
* This method should return quickly, and should not block if the renderer is currently unable to
|
||||
* make any useful progress.
|
||||
* <p>
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
||||
*
|
||||
* @param timeUs The current playback time.
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
protected abstract void doSomeWork(long timeUs) throws ExoPlaybackException;
|
||||
|
||||
/**
|
||||
* Returns the duration of the media being rendered.
|
||||
* <p>
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
||||
*
|
||||
* @return The duration of the track in micro-seconds, or {@link #MATCH_LONGEST} if
|
||||
* the track's duration should match that of the longest track whose duration is known, or
|
||||
* or {@link #UNKNOWN_TIME} if the duration is not known.
|
||||
*/
|
||||
protected abstract long getDurationUs();
|
||||
|
||||
/**
|
||||
* Returns the current playback position.
|
||||
* <p>
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
||||
*
|
||||
* @return The current playback position in micro-seconds.
|
||||
*/
|
||||
protected abstract long getCurrentPositionUs();
|
||||
|
||||
/**
|
||||
* Returns an estimate of the absolute position in micro-seconds up to which data is buffered.
|
||||
* <p>
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}
|
||||
*
|
||||
* @return An estimate of the absolute position in micro-seconds up to which data is buffered,
|
||||
* or {@link #END_OF_TRACK} if the track is fully buffered, or {@link #UNKNOWN_TIME} if no
|
||||
* estimate is available.
|
||||
*/
|
||||
protected abstract long getBufferedPositionUs();
|
||||
|
||||
/**
|
||||
* Seeks to a specified time in the track.
|
||||
* <p>
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_ENABLED}
|
||||
*
|
||||
* @param timeUs The desired time in micro-seconds.
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
protected abstract void seekTo(long timeUs) throws ExoPlaybackException;
|
||||
|
||||
@Override
|
||||
public void handleMessage(int what, Object object) throws ExoPlaybackException {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
/**
|
||||
* A SurfaceView that resizes itself to match a specified aspect ratio.
|
||||
*/
|
||||
public class VideoSurfaceView extends SurfaceView {
|
||||
|
||||
/**
|
||||
* The surface view will not resize itself if the fractional difference between its default
|
||||
* aspect ratio and the aspect ratio of the video falls below this threshold.
|
||||
* <p>
|
||||
* This tolerance is useful for fullscreen playbacks, since it ensures that the surface will
|
||||
* occupy the whole of the screen when playing content that has the same (or virtually the same)
|
||||
* aspect ratio as the device. This typically reduces the number of view layers that need to be
|
||||
* composited by the underlying system, which can help to reduce power consumption.
|
||||
*/
|
||||
private static final float MAX_ASPECT_RATIO_DEFORMATION_PERCENT = 0.01f;
|
||||
|
||||
private float videoAspectRatio;
|
||||
|
||||
public VideoSurfaceView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public VideoSurfaceView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the aspect ratio that this {@link VideoSurfaceView} should satisfy.
|
||||
*
|
||||
* @param widthHeightRatio The width to height ratio.
|
||||
*/
|
||||
public void setVideoWidthHeightRatio(float widthHeightRatio) {
|
||||
if (this.videoAspectRatio != widthHeightRatio) {
|
||||
this.videoAspectRatio = widthHeightRatio;
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
int width = getMeasuredWidth();
|
||||
int height = getMeasuredHeight();
|
||||
if (videoAspectRatio != 0) {
|
||||
float viewAspectRatio = (float) width / height;
|
||||
float aspectDeformation = videoAspectRatio / viewAspectRatio - 1;
|
||||
if (aspectDeformation > MAX_ASPECT_RATIO_DEFORMATION_PERCENT) {
|
||||
height = (int) (width / videoAspectRatio);
|
||||
} else if (aspectDeformation < -MAX_ASPECT_RATIO_DEFORMATION_PERCENT) {
|
||||
width = (int) (height * videoAspectRatio);
|
||||
}
|
||||
}
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
}
|
@ -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.chunk;
|
||||
|
||||
import com.google.android.exoplayer.upstream.Allocation;
|
||||
import com.google.android.exoplayer.upstream.Allocator;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSourceStream;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* An abstract base class for {@link Loadable} implementations that load chunks of data required
|
||||
* for the playback of streams.
|
||||
*/
|
||||
public abstract class Chunk implements Loadable {
|
||||
|
||||
/**
|
||||
* The format associated with the data being loaded.
|
||||
*/
|
||||
// TODO: Consider removing this and pushing it down into MediaChunk instead.
|
||||
public final Format format;
|
||||
/**
|
||||
* The reason for a {@link ChunkSource} having generated this chunk. For reporting only. Possible
|
||||
* values for this variable are defined by the specific {@link ChunkSource} implementations.
|
||||
*/
|
||||
public final int trigger;
|
||||
|
||||
private final DataSource dataSource;
|
||||
private final DataSpec dataSpec;
|
||||
|
||||
private DataSourceStream dataSourceStream;
|
||||
|
||||
/**
|
||||
* @param dataSource The source from which the data should be loaded.
|
||||
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
|
||||
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == DataSpec.LENGTH_UNBOUNDED} then
|
||||
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
|
||||
* {@link Integer#MAX_VALUE}.
|
||||
* @param format See {@link #format}.
|
||||
* @param trigger See {@link #trigger}.
|
||||
*/
|
||||
public Chunk(DataSource dataSource, DataSpec dataSpec, Format format, int trigger) {
|
||||
Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
|
||||
this.dataSource = Assertions.checkNotNull(dataSource);
|
||||
this.dataSpec = Assertions.checkNotNull(dataSpec);
|
||||
this.format = Assertions.checkNotNull(format);
|
||||
this.trigger = trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the {@link Chunk}.
|
||||
*
|
||||
* @param allocator An {@link Allocator} from which the {@link Allocation} needed to contain the
|
||||
* data can be obtained.
|
||||
*/
|
||||
public final void init(Allocator allocator) {
|
||||
Assertions.checkState(dataSourceStream == null);
|
||||
dataSourceStream = new DataSourceStream(dataSource, dataSpec, allocator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the {@link Chunk}, releasing any backing {@link Allocation}s.
|
||||
*/
|
||||
public final void release() {
|
||||
if (dataSourceStream != null) {
|
||||
dataSourceStream.close();
|
||||
dataSourceStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the length of the chunk in bytes.
|
||||
*
|
||||
* @return The length of the chunk in bytes, or {@value DataSpec#LENGTH_UNBOUNDED} if the length
|
||||
* has yet to be determined.
|
||||
*/
|
||||
public final long getLength() {
|
||||
return dataSourceStream.getLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the whole of the data has been consumed.
|
||||
*
|
||||
* @return True if the whole of the data has been consumed. False otherwise.
|
||||
*/
|
||||
public final boolean isReadFinished() {
|
||||
return dataSourceStream.isEndOfStream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the whole of the chunk has been loaded.
|
||||
*
|
||||
* @return True if the whole of the chunk has been loaded. False otherwise.
|
||||
*/
|
||||
public final boolean isLoadFinished() {
|
||||
return dataSourceStream.isLoadFinished();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of bytes that have been loaded.
|
||||
*
|
||||
* @return The number of bytes that have been loaded.
|
||||
*/
|
||||
public final long bytesLoaded() {
|
||||
return dataSourceStream.getLoadPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes loaded data to be consumed.
|
||||
*
|
||||
* @throws IOException If an error occurs consuming the loaded data.
|
||||
*/
|
||||
public final void consume() throws IOException {
|
||||
Assertions.checkState(dataSourceStream != null);
|
||||
consumeStream(dataSourceStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a byte array containing the loaded data. If the chunk is partially loaded, this
|
||||
* method returns the data that has been loaded so far. If nothing has been loaded, null is
|
||||
* returned.
|
||||
*
|
||||
* @return The loaded data or null.
|
||||
*/
|
||||
public final byte[] getLoadedData() {
|
||||
Assertions.checkState(dataSourceStream != null);
|
||||
return dataSourceStream.getLoadedData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked by {@link #consume()}. Implementations may override this method if they wish to
|
||||
* consume the loaded data at this point.
|
||||
* <p>
|
||||
* The default implementation is a no-op.
|
||||
*
|
||||
* @param stream The stream of loaded data.
|
||||
* @throws IOException If an error occurs consuming the loaded data.
|
||||
*/
|
||||
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
protected final NonBlockingInputStream getNonBlockingInputStream() {
|
||||
return dataSourceStream;
|
||||
}
|
||||
|
||||
protected final void resetReadPosition() {
|
||||
if (dataSourceStream != null) {
|
||||
dataSourceStream.resetReadPosition();
|
||||
} else {
|
||||
// We haven't been initialized yet, so the read position must already be 0.
|
||||
}
|
||||
}
|
||||
|
||||
// Loadable implementation
|
||||
|
||||
@Override
|
||||
public final void cancelLoad() {
|
||||
dataSourceStream.cancelLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isLoadCanceled() {
|
||||
return dataSourceStream.isLoadCanceled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void load() throws IOException, InterruptedException {
|
||||
dataSourceStream.load();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.chunk;
|
||||
|
||||
/**
|
||||
* Holds a chunk operation, which consists of a {@link Chunk} to load together with the number of
|
||||
* {@link MediaChunk}s that should be retained on the queue.
|
||||
*/
|
||||
public final class ChunkOperationHolder {
|
||||
|
||||
/**
|
||||
* The number of {@link MediaChunk}s to retain in a queue.
|
||||
*/
|
||||
public int queueSize;
|
||||
|
||||
/**
|
||||
* The chunk.
|
||||
*/
|
||||
public Chunk chunk;
|
||||
|
||||
}
|
@ -0,0 +1,753 @@
|
||||
/*
|
||||
* 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.chunk;
|
||||
|
||||
import com.google.android.exoplayer.FormatHolder;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.SampleSource;
|
||||
import com.google.android.exoplayer.TrackInfo;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.Loader;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link SampleSource} that loads media in {@link Chunk}s, which are themselves obtained from a
|
||||
* {@link ChunkSource}.
|
||||
*/
|
||||
public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be notified of {@link ChunkSampleSource} events.
|
||||
*/
|
||||
public interface EventListener {
|
||||
|
||||
/**
|
||||
* Invoked when an upstream load is started.
|
||||
*
|
||||
* @param sourceId The id of the reporting {@link SampleSource}.
|
||||
* @param formatId The format id.
|
||||
* @param trigger A trigger for the format selection, as specified by the {@link ChunkSource}.
|
||||
* @param isInitialization Whether the load is for format initialization data.
|
||||
* @param mediaStartTimeMs The media time of the start of the data being loaded, or -1 if this
|
||||
* load is for initialization data.
|
||||
* @param mediaEndTimeMs The media time of the end of the data being loaded, or -1 if this
|
||||
* load is for initialization data.
|
||||
* @param totalBytes The length of the data being loaded in bytes.
|
||||
*/
|
||||
void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization,
|
||||
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
|
||||
|
||||
/**
|
||||
* Invoked when the current load operation completes.
|
||||
*
|
||||
* @param sourceId The id of the reporting {@link SampleSource}.
|
||||
*/
|
||||
void onLoadCompleted(int sourceId);
|
||||
|
||||
/**
|
||||
* Invoked when the current upstream load operation is canceled.
|
||||
*
|
||||
* @param sourceId The id of the reporting {@link SampleSource}.
|
||||
*/
|
||||
void onLoadCanceled(int sourceId);
|
||||
|
||||
/**
|
||||
* Invoked when data is removed from the back of the buffer, typically so that it can be
|
||||
* re-buffered using a different representation.
|
||||
*
|
||||
* @param sourceId The id of the reporting {@link SampleSource}.
|
||||
* @param mediaStartTimeMs The media time of the start of the discarded data.
|
||||
* @param mediaEndTimeMs The media time of the end of the discarded data.
|
||||
* @param totalBytes The length of the data being discarded in bytes.
|
||||
*/
|
||||
void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
|
||||
long totalBytes);
|
||||
|
||||
/**
|
||||
* Invoked when an error occurs loading media data.
|
||||
*
|
||||
* @param sourceId The id of the reporting {@link SampleSource}.
|
||||
* @param e The cause of the failure.
|
||||
*/
|
||||
void onUpstreamError(int sourceId, IOException e);
|
||||
|
||||
/**
|
||||
* Invoked when an error occurs consuming loaded data.
|
||||
*
|
||||
* @param sourceId The id of the reporting {@link SampleSource}.
|
||||
* @param e The cause of the failure.
|
||||
*/
|
||||
void onConsumptionError(int sourceId, IOException e);
|
||||
|
||||
/**
|
||||
* Invoked when data is removed from the front of the buffer, typically due to a seek or
|
||||
* because the data has been consumed.
|
||||
*
|
||||
* @param sourceId The id of the reporting {@link SampleSource}.
|
||||
* @param mediaStartTimeMs The media time of the start of the discarded data.
|
||||
* @param mediaEndTimeMs The media time of the end of the discarded data.
|
||||
* @param totalBytes The length of the data being discarded in bytes.
|
||||
*/
|
||||
void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
|
||||
long totalBytes);
|
||||
|
||||
/**
|
||||
* Invoked when the downstream format changes (i.e. when the format being supplied to the
|
||||
* caller of {@link SampleSource#readData} changes).
|
||||
*
|
||||
* @param sourceId The id of the reporting {@link SampleSource}.
|
||||
* @param formatId The format id.
|
||||
* @param trigger The trigger specified in the corresponding upstream load, as specified by the
|
||||
* {@link ChunkSource}.
|
||||
* @param mediaTimeMs The media time at which the change occurred.
|
||||
*/
|
||||
void onDownstreamFormatChanged(int sourceId, int formatId, int trigger, int mediaTimeMs);
|
||||
|
||||
}
|
||||
|
||||
private static final int STATE_UNPREPARED = 0;
|
||||
private static final int STATE_PREPARED = 1;
|
||||
private static final int STATE_ENABLED = 2;
|
||||
|
||||
private static final int NO_RESET_PENDING = -1;
|
||||
|
||||
private final int eventSourceId;
|
||||
private final LoadControl loadControl;
|
||||
private final ChunkSource chunkSource;
|
||||
private final ChunkOperationHolder currentLoadableHolder;
|
||||
private final LinkedList<MediaChunk> mediaChunks;
|
||||
private final List<MediaChunk> readOnlyMediaChunks;
|
||||
private final int bufferSizeContribution;
|
||||
private final boolean frameAccurateSeeking;
|
||||
private final Handler eventHandler;
|
||||
private final EventListener eventListener;
|
||||
|
||||
private int state;
|
||||
private long downstreamPositionUs;
|
||||
private long lastSeekPositionUs;
|
||||
private long pendingResetTime;
|
||||
private long lastPerformedBufferOperation;
|
||||
private boolean pendingDiscontinuity;
|
||||
|
||||
private Loader loader;
|
||||
private IOException currentLoadableException;
|
||||
private boolean currentLoadableExceptionFatal;
|
||||
private int currentLoadableExceptionCount;
|
||||
private long currentLoadableExceptionTimestamp;
|
||||
|
||||
private volatile Format downstreamFormat;
|
||||
|
||||
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
|
||||
int bufferSizeContribution, boolean frameAccurateSeeking) {
|
||||
this(chunkSource, loadControl, bufferSizeContribution, frameAccurateSeeking, null, null, 0);
|
||||
}
|
||||
|
||||
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
|
||||
int bufferSizeContribution, boolean frameAccurateSeeking, Handler eventHandler,
|
||||
EventListener eventListener, int eventSourceId) {
|
||||
this.chunkSource = chunkSource;
|
||||
this.loadControl = loadControl;
|
||||
this.bufferSizeContribution = bufferSizeContribution;
|
||||
this.frameAccurateSeeking = frameAccurateSeeking;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
this.eventSourceId = eventSourceId;
|
||||
currentLoadableHolder = new ChunkOperationHolder();
|
||||
mediaChunks = new LinkedList<MediaChunk>();
|
||||
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
|
||||
state = STATE_UNPREPARED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes the current downstream format for debugging purposes. Can be called from any thread.
|
||||
*
|
||||
* @return The current downstream format.
|
||||
*/
|
||||
public Format getFormat() {
|
||||
return downstreamFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare() {
|
||||
Assertions.checkState(state == STATE_UNPREPARED);
|
||||
loader = new Loader("Loader:" + chunkSource.getTrackInfo().mimeType, this);
|
||||
state = STATE_PREPARED;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTrackCount() {
|
||||
Assertions.checkState(state != STATE_UNPREPARED);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackInfo getTrackInfo(int track) {
|
||||
Assertions.checkState(state != STATE_UNPREPARED);
|
||||
Assertions.checkState(track == 0);
|
||||
return chunkSource.getTrackInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable(int track, long timeUs) {
|
||||
Assertions.checkState(state == STATE_PREPARED);
|
||||
Assertions.checkState(track == 0);
|
||||
state = STATE_ENABLED;
|
||||
chunkSource.enable();
|
||||
loadControl.register(this, bufferSizeContribution);
|
||||
downstreamFormat = null;
|
||||
downstreamPositionUs = timeUs;
|
||||
lastSeekPositionUs = timeUs;
|
||||
restartFrom(timeUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable(int track) {
|
||||
Assertions.checkState(state == STATE_ENABLED);
|
||||
Assertions.checkState(track == 0);
|
||||
pendingDiscontinuity = false;
|
||||
state = STATE_PREPARED;
|
||||
loadControl.unregister(this);
|
||||
chunkSource.disable(mediaChunks);
|
||||
if (loader.isLoading()) {
|
||||
loader.cancelLoading();
|
||||
} else {
|
||||
clearMediaChunks();
|
||||
clearCurrentLoadable();
|
||||
loadControl.trimAllocator();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void continueBuffering(long playbackPositionUs) {
|
||||
Assertions.checkState(state == STATE_ENABLED);
|
||||
downstreamPositionUs = playbackPositionUs;
|
||||
chunkSource.continueBuffering(playbackPositionUs);
|
||||
updateLoadControl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
|
||||
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
|
||||
Assertions.checkState(state == STATE_ENABLED);
|
||||
Assertions.checkState(track == 0);
|
||||
|
||||
if (pendingDiscontinuity) {
|
||||
pendingDiscontinuity = false;
|
||||
return DISCONTINUITY_READ;
|
||||
}
|
||||
|
||||
if (onlyReadDiscontinuity) {
|
||||
return NOTHING_READ;
|
||||
}
|
||||
|
||||
downstreamPositionUs = playbackPositionUs;
|
||||
if (isPendingReset()) {
|
||||
if (currentLoadableException != null) {
|
||||
throw currentLoadableException;
|
||||
}
|
||||
IOException chunkSourceException = chunkSource.getError();
|
||||
if (chunkSourceException != null) {
|
||||
throw chunkSourceException;
|
||||
}
|
||||
return NOTHING_READ;
|
||||
}
|
||||
|
||||
MediaChunk mediaChunk = mediaChunks.getFirst();
|
||||
if (mediaChunk.isReadFinished()) {
|
||||
// We've read all of the samples from the current media chunk.
|
||||
if (mediaChunks.size() > 1) {
|
||||
discardDownstreamMediaChunk();
|
||||
mediaChunk = mediaChunks.getFirst();
|
||||
mediaChunk.seekToStart();
|
||||
return readData(track, playbackPositionUs, formatHolder, sampleHolder, false);
|
||||
} else if (mediaChunk.isLastChunk()) {
|
||||
return END_OF_STREAM;
|
||||
} else {
|
||||
IOException chunkSourceException = chunkSource.getError();
|
||||
if (chunkSourceException != null) {
|
||||
throw chunkSourceException;
|
||||
}
|
||||
return NOTHING_READ;
|
||||
}
|
||||
} else if (downstreamFormat == null || downstreamFormat.id != mediaChunk.format.id) {
|
||||
notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger,
|
||||
mediaChunk.startTimeUs);
|
||||
MediaFormat format = mediaChunk.getMediaFormat();
|
||||
chunkSource.getMaxVideoDimensions(format);
|
||||
formatHolder.format = format;
|
||||
formatHolder.drmInitData = mediaChunk.getPsshInfo();
|
||||
downstreamFormat = mediaChunk.format;
|
||||
return FORMAT_READ;
|
||||
}
|
||||
|
||||
if (mediaChunk.read(sampleHolder)) {
|
||||
sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
|
||||
onSampleRead(mediaChunk, sampleHolder);
|
||||
return SAMPLE_READ;
|
||||
} else {
|
||||
if (currentLoadableException != null) {
|
||||
throw currentLoadableException;
|
||||
}
|
||||
return NOTHING_READ;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToUs(long timeUs) {
|
||||
Assertions.checkState(state == STATE_ENABLED);
|
||||
downstreamPositionUs = timeUs;
|
||||
lastSeekPositionUs = timeUs;
|
||||
if (pendingResetTime == timeUs) {
|
||||
return;
|
||||
}
|
||||
|
||||
MediaChunk mediaChunk = getMediaChunk(timeUs);
|
||||
if (mediaChunk == null) {
|
||||
restartFrom(timeUs);
|
||||
pendingDiscontinuity = true;
|
||||
} else {
|
||||
pendingDiscontinuity |= mediaChunk.seekTo(timeUs, mediaChunk == mediaChunks.getFirst());
|
||||
discardDownstreamMediaChunks(mediaChunk);
|
||||
updateLoadControl();
|
||||
}
|
||||
}
|
||||
|
||||
private MediaChunk getMediaChunk(long timeUs) {
|
||||
Iterator<MediaChunk> mediaChunkIterator = mediaChunks.iterator();
|
||||
while (mediaChunkIterator.hasNext()) {
|
||||
MediaChunk mediaChunk = mediaChunkIterator.next();
|
||||
if (timeUs < mediaChunk.startTimeUs) {
|
||||
return null;
|
||||
} else if (mediaChunk.isLastChunk() || timeUs < mediaChunk.endTimeUs) {
|
||||
return mediaChunk;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPositionUs() {
|
||||
Assertions.checkState(state == STATE_ENABLED);
|
||||
if (isPendingReset()) {
|
||||
return pendingResetTime;
|
||||
}
|
||||
MediaChunk mediaChunk = mediaChunks.getLast();
|
||||
Chunk currentLoadable = currentLoadableHolder.chunk;
|
||||
if (currentLoadable != null && mediaChunk == currentLoadable) {
|
||||
// Linearly interpolate partially-fetched chunk times.
|
||||
long chunkLength = mediaChunk.getLength();
|
||||
if (chunkLength != DataSpec.LENGTH_UNBOUNDED) {
|
||||
return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) *
|
||||
mediaChunk.bytesLoaded()) / chunkLength;
|
||||
} else {
|
||||
return mediaChunk.startTimeUs;
|
||||
}
|
||||
} else if (mediaChunk.isLastChunk()) {
|
||||
return TrackRenderer.END_OF_TRACK;
|
||||
} else {
|
||||
return mediaChunk.endTimeUs;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
Assertions.checkState(state != STATE_ENABLED);
|
||||
if (loader != null) {
|
||||
loader.release();
|
||||
loader = null;
|
||||
}
|
||||
state = STATE_UNPREPARED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaded() {
|
||||
Chunk currentLoadable = currentLoadableHolder.chunk;
|
||||
try {
|
||||
currentLoadable.consume();
|
||||
} catch (IOException e) {
|
||||
currentLoadableException = e;
|
||||
currentLoadableExceptionCount++;
|
||||
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
|
||||
currentLoadableExceptionFatal = true;
|
||||
notifyConsumptionError(e);
|
||||
} finally {
|
||||
if (!isMediaChunk(currentLoadable)) {
|
||||
currentLoadable.release();
|
||||
}
|
||||
if (!currentLoadableExceptionFatal) {
|
||||
clearCurrentLoadable();
|
||||
}
|
||||
notifyLoadCompleted();
|
||||
updateLoadControl();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
Chunk currentLoadable = currentLoadableHolder.chunk;
|
||||
if (!isMediaChunk(currentLoadable)) {
|
||||
currentLoadable.release();
|
||||
}
|
||||
clearCurrentLoadable();
|
||||
notifyLoadCanceled();
|
||||
if (state == STATE_ENABLED) {
|
||||
restartFrom(pendingResetTime);
|
||||
} else {
|
||||
clearMediaChunks();
|
||||
loadControl.trimAllocator();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(IOException e) {
|
||||
currentLoadableException = e;
|
||||
currentLoadableExceptionCount++;
|
||||
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
|
||||
notifyUpstreamError(e);
|
||||
updateLoadControl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a sample has been read from a {@link MediaChunk}. Can be used to perform any
|
||||
* modifications necessary before the sample is returned.
|
||||
*
|
||||
* @param mediaChunk The MediaChunk the sample was ready from.
|
||||
* @param sampleHolder The sample that has just been read.
|
||||
*/
|
||||
protected void onSampleRead(MediaChunk mediaChunk, SampleHolder sampleHolder) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
private void restartFrom(long timeUs) {
|
||||
pendingResetTime = timeUs;
|
||||
if (loader.isLoading()) {
|
||||
loader.cancelLoading();
|
||||
} else {
|
||||
clearMediaChunks();
|
||||
clearCurrentLoadable();
|
||||
updateLoadControl();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearMediaChunks() {
|
||||
discardDownstreamMediaChunks(null);
|
||||
}
|
||||
|
||||
private void clearCurrentLoadable() {
|
||||
currentLoadableHolder.chunk = null;
|
||||
currentLoadableException = null;
|
||||
currentLoadableExceptionCount = 0;
|
||||
currentLoadableExceptionFatal = false;
|
||||
}
|
||||
|
||||
private void updateLoadControl() {
|
||||
long loadPositionUs;
|
||||
if (isPendingReset()) {
|
||||
loadPositionUs = pendingResetTime;
|
||||
} else {
|
||||
MediaChunk lastMediaChunk = mediaChunks.getLast();
|
||||
loadPositionUs = lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs;
|
||||
}
|
||||
|
||||
boolean isBackedOff = currentLoadableException != null && !currentLoadableExceptionFatal;
|
||||
boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs,
|
||||
isBackedOff || loader.isLoading(), currentLoadableExceptionFatal);
|
||||
|
||||
if (currentLoadableExceptionFatal) {
|
||||
return;
|
||||
}
|
||||
|
||||
long now = SystemClock.elapsedRealtime();
|
||||
|
||||
if (isBackedOff) {
|
||||
long elapsedMillis = now - currentLoadableExceptionTimestamp;
|
||||
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
|
||||
resumeFromBackOff();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!loader.isLoading()) {
|
||||
if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) {
|
||||
lastPerformedBufferOperation = now;
|
||||
currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
|
||||
chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs,
|
||||
currentLoadableHolder);
|
||||
discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
|
||||
}
|
||||
if (nextLoader) {
|
||||
maybeStartLoading();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes loading.
|
||||
* <p>
|
||||
* If the {@link ChunkSource} returns a chunk equivalent to the backed off chunk B, then the
|
||||
* loading of B will be resumed. In all other cases B will be discarded and the new chunk will
|
||||
* be loaded.
|
||||
*/
|
||||
private void resumeFromBackOff() {
|
||||
currentLoadableException = null;
|
||||
|
||||
Chunk backedOffChunk = currentLoadableHolder.chunk;
|
||||
if (!isMediaChunk(backedOffChunk)) {
|
||||
currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
|
||||
chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs,
|
||||
currentLoadableHolder);
|
||||
discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
|
||||
if (currentLoadableHolder.chunk == backedOffChunk) {
|
||||
// Chunk was unchanged. Resume loading.
|
||||
loader.startLoading(backedOffChunk);
|
||||
} else {
|
||||
backedOffChunk.release();
|
||||
maybeStartLoading();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (backedOffChunk == mediaChunks.getFirst()) {
|
||||
// We're not able to clear the first media chunk, so we have no choice but to continue
|
||||
// loading it.
|
||||
loader.startLoading(backedOffChunk);
|
||||
return;
|
||||
}
|
||||
|
||||
// The current loadable is the last media chunk. Remove it before we invoke the chunk source,
|
||||
// and add it back again afterwards.
|
||||
MediaChunk removedChunk = mediaChunks.removeLast();
|
||||
Assertions.checkState(backedOffChunk == removedChunk);
|
||||
currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
|
||||
chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs,
|
||||
currentLoadableHolder);
|
||||
mediaChunks.add(removedChunk);
|
||||
|
||||
if (currentLoadableHolder.chunk == backedOffChunk) {
|
||||
// Chunk was unchanged. Resume loading.
|
||||
loader.startLoading(backedOffChunk);
|
||||
} else {
|
||||
// This call will remove and release at least one chunk from the end of mediaChunks. Since
|
||||
// the current loadable is the last media chunk, it is guaranteed to be removed.
|
||||
discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
|
||||
clearCurrentLoadable();
|
||||
maybeStartLoading();
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeStartLoading() {
|
||||
Chunk currentLoadable = currentLoadableHolder.chunk;
|
||||
if (currentLoadable == null) {
|
||||
// Nothing to load.
|
||||
return;
|
||||
}
|
||||
currentLoadable.init(loadControl.getAllocator());
|
||||
if (isMediaChunk(currentLoadable)) {
|
||||
MediaChunk mediaChunk = (MediaChunk) currentLoadable;
|
||||
if (isPendingReset()) {
|
||||
mediaChunk.seekTo(pendingResetTime, false);
|
||||
pendingResetTime = NO_RESET_PENDING;
|
||||
}
|
||||
mediaChunks.add(mediaChunk);
|
||||
notifyLoadStarted(mediaChunk.format.id, mediaChunk.trigger, false,
|
||||
mediaChunk.startTimeUs, mediaChunk.endTimeUs, mediaChunk.getLength());
|
||||
} else {
|
||||
notifyLoadStarted(currentLoadable.format.id, currentLoadable.trigger, true, -1, -1,
|
||||
currentLoadable.getLength());
|
||||
}
|
||||
loader.startLoading(currentLoadable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not
|
||||
* itself discarded. Null can be passed to discard all media chunks.
|
||||
*
|
||||
* @param untilChunk The first media chunk to keep, or null to discard all media chunks.
|
||||
*/
|
||||
private void discardDownstreamMediaChunks(MediaChunk untilChunk) {
|
||||
if (mediaChunks.isEmpty() || untilChunk == mediaChunks.getFirst()) {
|
||||
return;
|
||||
}
|
||||
long totalBytes = 0;
|
||||
long startTimeUs = mediaChunks.getFirst().startTimeUs;
|
||||
long endTimeUs = 0;
|
||||
while (!mediaChunks.isEmpty() && untilChunk != mediaChunks.getFirst()) {
|
||||
MediaChunk removed = mediaChunks.removeFirst();
|
||||
totalBytes += removed.bytesLoaded();
|
||||
endTimeUs = removed.endTimeUs;
|
||||
removed.release();
|
||||
}
|
||||
notifyDownstreamDiscarded(startTimeUs, endTimeUs, totalBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards the first downstream media chunk.
|
||||
*/
|
||||
private void discardDownstreamMediaChunk() {
|
||||
MediaChunk removed = mediaChunks.removeFirst();
|
||||
long totalBytes = removed.bytesLoaded();
|
||||
removed.release();
|
||||
notifyDownstreamDiscarded(removed.startTimeUs, removed.endTimeUs, totalBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard upstream media chunks until the queue length is equal to the length specified.
|
||||
*
|
||||
* @param queueLength The desired length of the queue.
|
||||
*/
|
||||
private void discardUpstreamMediaChunks(int queueLength) {
|
||||
if (mediaChunks.size() <= queueLength) {
|
||||
return;
|
||||
}
|
||||
long totalBytes = 0;
|
||||
long startTimeUs = 0;
|
||||
long endTimeUs = mediaChunks.getLast().endTimeUs;
|
||||
while (mediaChunks.size() > queueLength) {
|
||||
MediaChunk removed = mediaChunks.removeLast();
|
||||
totalBytes += removed.bytesLoaded();
|
||||
startTimeUs = removed.startTimeUs;
|
||||
removed.release();
|
||||
}
|
||||
notifyUpstreamDiscarded(startTimeUs, endTimeUs, totalBytes);
|
||||
}
|
||||
|
||||
private boolean isMediaChunk(Chunk chunk) {
|
||||
return chunk instanceof MediaChunk;
|
||||
}
|
||||
|
||||
private boolean isPendingReset() {
|
||||
return pendingResetTime != NO_RESET_PENDING;
|
||||
}
|
||||
|
||||
private long getRetryDelayMillis(long errorCount) {
|
||||
return Math.min((errorCount - 1) * 1000, 5000);
|
||||
}
|
||||
|
||||
protected final int usToMs(long timeUs) {
|
||||
return (int) (timeUs / 1000);
|
||||
}
|
||||
|
||||
private void notifyLoadStarted(final int formatId, final int trigger,
|
||||
final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs,
|
||||
final long totalBytes) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onLoadStarted(eventSourceId, formatId, trigger, isInitialization,
|
||||
usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), totalBytes);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyLoadCompleted() {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onLoadCompleted(eventSourceId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyLoadCanceled() {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onLoadCanceled(eventSourceId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyUpstreamError(final IOException e) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onUpstreamError(eventSourceId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyConsumptionError(final IOException e) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onConsumptionError(eventSourceId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyUpstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs,
|
||||
final long totalBytes) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onUpstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs),
|
||||
usToMs(mediaEndTimeUs), totalBytes);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyDownstreamFormatChanged(final int formatId, final int trigger,
|
||||
final long mediaTimeUs) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onDownstreamFormatChanged(eventSourceId, formatId, trigger,
|
||||
usToMs(mediaTimeUs));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyDownstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs,
|
||||
final long totalBytes) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onDownstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs),
|
||||
usToMs(mediaEndTimeUs), totalBytes);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.chunk;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.TrackInfo;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A provider of {@link Chunk}s for a {@link ChunkSampleSource} to load.
|
||||
*/
|
||||
/*
|
||||
* TODO: Share more state between this interface and {@link ChunkSampleSource}. In particular
|
||||
* implementations of this class needs to know about errors, and should be more tightly integrated
|
||||
* into the process of resuming loading of a chunk after an error occurs.
|
||||
*/
|
||||
public interface ChunkSource {
|
||||
|
||||
/**
|
||||
* Gets information about the track for which this instance provides {@link Chunk}s.
|
||||
* <p>
|
||||
* May be called when the source is disabled or enabled.
|
||||
*
|
||||
* @return Information about the track.
|
||||
*/
|
||||
TrackInfo getTrackInfo();
|
||||
|
||||
/**
|
||||
* Adaptive video {@link ChunkSource} implementations must set the maximum video dimensions on
|
||||
* the supplied {@link MediaFormat}. Other implementations do nothing.
|
||||
* <p>
|
||||
* Only called when the source is enabled.
|
||||
*/
|
||||
void getMaxVideoDimensions(MediaFormat out);
|
||||
|
||||
/**
|
||||
* Called when the source is enabled.
|
||||
*/
|
||||
void enable();
|
||||
|
||||
/**
|
||||
* Called when the source is disabled.
|
||||
*
|
||||
* @param queue A representation of the currently buffered {@link MediaChunk}s.
|
||||
*/
|
||||
void disable(List<MediaChunk> queue);
|
||||
|
||||
/**
|
||||
* Indicates to the source that it should still be checking for updates to the stream.
|
||||
*
|
||||
* @param playbackPositionUs The current playback position.
|
||||
*/
|
||||
void continueBuffering(long playbackPositionUs);
|
||||
|
||||
/**
|
||||
* Updates the provided {@link ChunkOperationHolder} to contain the next operation that should
|
||||
* be performed by the calling {@link ChunkSampleSource}.
|
||||
* <p>
|
||||
* The next operation comprises of a possibly shortened queue length (shortened if the
|
||||
* implementation wishes for the caller to discard {@link MediaChunk}s from the queue), together
|
||||
* with the next {@link Chunk} to load. The next chunk may be a {@link MediaChunk} to be added to
|
||||
* the queue, or another {@link Chunk} type (e.g. to load initialization data), or null if the
|
||||
* source is not able to provide a chunk in its current state.
|
||||
*
|
||||
* @param queue A representation of the currently buffered {@link MediaChunk}s.
|
||||
* @param seekPositionUs If the queue is empty, this parameter must specify the seek position. If
|
||||
* the queue is non-empty then this parameter is ignored.
|
||||
* @param playbackPositionUs The current playback position.
|
||||
* @param out A holder for the next operation, whose {@link ChunkOperationHolder#queueSize} is
|
||||
* initially equal to the length of the queue, and whose {@link ChunkOperationHolder#chunk} is
|
||||
* initially equal to null or a {@link Chunk} previously supplied by the {@link ChunkSource}
|
||||
* that the caller has not yet finished loading. In the latter case the chunk can either be
|
||||
* replaced or left unchanged. Note that leaving the chunk unchanged is both preferred and
|
||||
* more efficient than replacing it with a new but identical chunk.
|
||||
*/
|
||||
void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
|
||||
long playbackPositionUs, ChunkOperationHolder out);
|
||||
|
||||
/**
|
||||
* If the {@link ChunkSource} is currently unable to provide chunks through
|
||||
* {@link ChunkSource#getChunkOperation}, then this method returns the underlying cause. Returns
|
||||
* null otherwise.
|
||||
*
|
||||
* @return An {@link IOException}, or null.
|
||||
*/
|
||||
IOException getError();
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.chunk;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* A format definition for streams.
|
||||
*/
|
||||
public final class Format {
|
||||
|
||||
/**
|
||||
* Sorts {@link Format} objects in order of decreasing bandwidth.
|
||||
*/
|
||||
public static final class DecreasingBandwidthComparator implements Comparator<Format> {
|
||||
|
||||
@Override
|
||||
public int compare(Format a, Format b) {
|
||||
return b.bandwidth - a.bandwidth;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An identifier for the format.
|
||||
*/
|
||||
public final int id;
|
||||
|
||||
/**
|
||||
* The mime type of the format.
|
||||
*/
|
||||
public final String mimeType;
|
||||
|
||||
/**
|
||||
* The width of the video in pixels, or -1 for non-video formats.
|
||||
*/
|
||||
public final int width;
|
||||
|
||||
/**
|
||||
* The height of the video in pixels, or -1 for non-video formats.
|
||||
*/
|
||||
public final int height;
|
||||
|
||||
/**
|
||||
* The number of audio channels, or -1 for non-audio formats.
|
||||
*/
|
||||
public final int numChannels;
|
||||
|
||||
/**
|
||||
* The audio sampling rate in Hz, or -1 for non-audio formats.
|
||||
*/
|
||||
public final int audioSamplingRate;
|
||||
|
||||
/**
|
||||
* The average bandwidth in bytes per second.
|
||||
*/
|
||||
public final int bandwidth;
|
||||
|
||||
/**
|
||||
* @param id The format identifier.
|
||||
* @param mimeType The format mime type.
|
||||
* @param width The width of the video in pixels, or -1 for non-video formats.
|
||||
* @param height The height of the video in pixels, or -1 for non-video formats.
|
||||
* @param numChannels The number of audio channels, or -1 for non-audio formats.
|
||||
* @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats.
|
||||
* @param bandwidth The average bandwidth of the format in bytes per second.
|
||||
*/
|
||||
public Format(int id, String mimeType, int width, int height, int numChannels,
|
||||
int audioSamplingRate, int bandwidth) {
|
||||
this.id = id;
|
||||
this.mimeType = mimeType;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.numChannels = numChannels;
|
||||
this.audioSamplingRate = audioSamplingRate;
|
||||
this.bandwidth = bandwidth;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,301 @@
|
||||
/*
|
||||
* 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.chunk;
|
||||
|
||||
import com.google.android.exoplayer.upstream.BandwidthMeter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Selects from a number of available formats during playback.
|
||||
*/
|
||||
public interface FormatEvaluator {
|
||||
|
||||
/**
|
||||
* The trigger for the initial format selection.
|
||||
*/
|
||||
static final int TRIGGER_INITIAL = 0;
|
||||
/**
|
||||
* The trigger for a format selection that was triggered by the user.
|
||||
*/
|
||||
static final int TRIGGER_MANUAL = 1;
|
||||
/**
|
||||
* The trigger for an adaptive format selection.
|
||||
*/
|
||||
static final int TRIGGER_ADAPTIVE = 2;
|
||||
/**
|
||||
* Implementations may define custom trigger codes greater than or equal to this value.
|
||||
*/
|
||||
static final int TRIGGER_CUSTOM_BASE = 10000;
|
||||
|
||||
/**
|
||||
* Enables the evaluator.
|
||||
*/
|
||||
void enable();
|
||||
|
||||
/**
|
||||
* Disables the evaluator.
|
||||
*/
|
||||
void disable();
|
||||
|
||||
/**
|
||||
* Update the supplied evaluation.
|
||||
* <p>
|
||||
* When the method is invoked, {@code evaluation} will contain the currently selected
|
||||
* format (null for the first evaluation), the most recent trigger (TRIGGER_INITIAL for the
|
||||
* first evaluation) and the current queue size. The implementation should update these
|
||||
* fields as necessary.
|
||||
* <p>
|
||||
* The trigger should be considered "sticky" for as long as a given representation is selected,
|
||||
* and so should only be changed if the representation is also changed.
|
||||
*
|
||||
* @param queue A read only representation of the currently buffered {@link MediaChunk}s.
|
||||
* @param playbackPositionUs The current playback position.
|
||||
* @param formats The formats from which to select, ordered by decreasing bandwidth.
|
||||
* @param evaluation The evaluation.
|
||||
*/
|
||||
// TODO: Pass more useful information into this method, and finalize the interface.
|
||||
void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs, Format[] formats,
|
||||
Evaluation evaluation);
|
||||
|
||||
/**
|
||||
* A format evaluation.
|
||||
*/
|
||||
public static final class Evaluation {
|
||||
|
||||
/**
|
||||
* The desired size of the queue.
|
||||
*/
|
||||
public int queueSize;
|
||||
|
||||
/**
|
||||
* The sticky reason for the format selection.
|
||||
*/
|
||||
public int trigger;
|
||||
|
||||
/**
|
||||
* The selected format.
|
||||
*/
|
||||
public Format format;
|
||||
|
||||
public Evaluation() {
|
||||
trigger = TRIGGER_INITIAL;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Always selects the first format.
|
||||
*/
|
||||
public static class FixedEvaluator implements FormatEvaluator {
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
|
||||
Format[] formats, Evaluation evaluation) {
|
||||
evaluation.format = formats[0];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects randomly between the available formats.
|
||||
*/
|
||||
public static class RandomEvaluator implements FormatEvaluator {
|
||||
|
||||
private final Random random;
|
||||
|
||||
public RandomEvaluator() {
|
||||
this.random = new Random();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
|
||||
Format[] formats, Evaluation evaluation) {
|
||||
Format newFormat = formats[random.nextInt(formats.length)];
|
||||
if (evaluation.format != null && evaluation.format.id != newFormat.id) {
|
||||
evaluation.trigger = TRIGGER_ADAPTIVE;
|
||||
}
|
||||
evaluation.format = newFormat;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An adaptive evaluator for video formats, which attempts to select the best quality possible
|
||||
* given the current network conditions and state of the buffer.
|
||||
* <p>
|
||||
* This implementation should be used for video only, and should not be used for audio. It is a
|
||||
* reference implementation only. It is recommended that application developers implement their
|
||||
* own adaptive evaluator to more precisely suit their use case.
|
||||
*/
|
||||
public static class AdaptiveEvaluator implements FormatEvaluator {
|
||||
|
||||
public static final int DEFAULT_MAX_INITIAL_BYTE_RATE = 100000;
|
||||
|
||||
public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
|
||||
public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
|
||||
public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;
|
||||
public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f;
|
||||
|
||||
private final BandwidthMeter bandwidthMeter;
|
||||
|
||||
private final int maxInitialByteRate;
|
||||
private final long minDurationForQualityIncreaseUs;
|
||||
private final long maxDurationForQualityDecreaseUs;
|
||||
private final long minDurationToRetainAfterDiscardUs;
|
||||
private final float bandwidthFraction;
|
||||
|
||||
/**
|
||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
||||
*/
|
||||
public AdaptiveEvaluator(BandwidthMeter bandwidthMeter) {
|
||||
this (bandwidthMeter, DEFAULT_MAX_INITIAL_BYTE_RATE,
|
||||
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
|
||||
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
||||
* @param maxInitialByteRate The maximum bandwidth in bytes per second that should be assumed
|
||||
* when bandwidthMeter cannot provide an estimate due to playback having only just started.
|
||||
* @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for
|
||||
* the evaluator to consider switching to a higher quality format.
|
||||
* @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for
|
||||
* the evaluator to consider switching to a lower quality format.
|
||||
* @param minDurationToRetainAfterDiscardMs When switching to a significantly higher quality
|
||||
* format, the evaluator may discard some of the media that it has already buffered at the
|
||||
* lower quality, so as to switch up to the higher quality faster. This is the minimum
|
||||
* duration of media that must be retained at the lower quality.
|
||||
* @param bandwidthFraction The fraction of the available bandwidth that the evaluator should
|
||||
* consider available for use. Setting to a value less than 1 is recommended to account
|
||||
* for inaccuracies in the bandwidth estimator.
|
||||
*/
|
||||
public AdaptiveEvaluator(BandwidthMeter bandwidthMeter,
|
||||
int maxInitialByteRate,
|
||||
int minDurationForQualityIncreaseMs,
|
||||
int maxDurationForQualityDecreaseMs,
|
||||
int minDurationToRetainAfterDiscardMs,
|
||||
float bandwidthFraction) {
|
||||
this.bandwidthMeter = bandwidthMeter;
|
||||
this.maxInitialByteRate = maxInitialByteRate;
|
||||
this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
|
||||
this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
|
||||
this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
|
||||
this.bandwidthFraction = bandwidthFraction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
|
||||
Format[] formats, Evaluation evaluation) {
|
||||
long bufferedDurationUs = queue.isEmpty() ? 0
|
||||
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
|
||||
Format current = evaluation.format;
|
||||
Format ideal = determineIdealFormat(formats, bandwidthMeter.getEstimate());
|
||||
boolean isHigher = ideal != null && current != null && ideal.bandwidth > current.bandwidth;
|
||||
boolean isLower = ideal != null && current != null && ideal.bandwidth < current.bandwidth;
|
||||
if (isHigher) {
|
||||
if (bufferedDurationUs < minDurationForQualityIncreaseUs) {
|
||||
// The ideal format is a higher quality, but we have insufficient buffer to
|
||||
// safely switch up. Defer switching up for now.
|
||||
ideal = current;
|
||||
} else if (bufferedDurationUs >= minDurationToRetainAfterDiscardUs) {
|
||||
// We're switching from an SD stream to a stream of higher resolution. Consider
|
||||
// discarding already buffered media chunks. Specifically, discard media chunks starting
|
||||
// from the first one that is of lower bandwidth, lower resolution and that is not HD.
|
||||
for (int i = 0; i < queue.size(); i++) {
|
||||
MediaChunk thisChunk = queue.get(i);
|
||||
long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
|
||||
if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
|
||||
&& thisChunk.format.bandwidth < ideal.bandwidth
|
||||
&& thisChunk.format.height < ideal.height
|
||||
&& thisChunk.format.height < 720
|
||||
&& thisChunk.format.width < 1280) {
|
||||
// Discard chunks from this one onwards.
|
||||
evaluation.queueSize = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isLower && current != null
|
||||
&& bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
|
||||
// The ideal format is a lower quality, but we have sufficient buffer to defer switching
|
||||
// down for now.
|
||||
ideal = current;
|
||||
}
|
||||
if (current != null && ideal != current) {
|
||||
evaluation.trigger = FormatEvaluator.TRIGGER_ADAPTIVE;
|
||||
}
|
||||
evaluation.format = ideal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the ideal format ignoring buffer health.
|
||||
*/
|
||||
protected Format determineIdealFormat(Format[] formats, long bandwidthEstimate) {
|
||||
long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate);
|
||||
for (int i = 0; i < formats.length; i++) {
|
||||
Format format = formats[i];
|
||||
if (format.bandwidth <= effectiveBandwidth) {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
// We didn't manage to calculate a suitable format. Return the lowest quality format.
|
||||
return formats[formats.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply overhead factor, or default value in absence of estimate.
|
||||
*/
|
||||
protected long computeEffectiveBandwidthEstimate(long bandwidthEstimate) {
|
||||
return bandwidthEstimate == BandwidthMeter.NO_ESTIMATE
|
||||
? maxInitialByteRate : (long) (bandwidthEstimate * bandwidthFraction);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.chunk;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* An abstract base class for {@link Chunk}s that contain media samples.
|
||||
*/
|
||||
public abstract class MediaChunk extends Chunk {
|
||||
|
||||
/**
|
||||
* The start time of the media contained by the chunk.
|
||||
*/
|
||||
public final long startTimeUs;
|
||||
/**
|
||||
* The end time of the media contained by the chunk.
|
||||
*/
|
||||
public final long endTimeUs;
|
||||
/**
|
||||
* The index of the next media chunk, or -1 if this is the last media chunk in the stream.
|
||||
*/
|
||||
public final int nextChunkIndex;
|
||||
|
||||
/**
|
||||
* Constructor for a chunk of media samples.
|
||||
*
|
||||
* @param dataSource A {@link DataSource} for loading the data.
|
||||
* @param dataSpec Defines the data to be loaded.
|
||||
* @param format The format of the stream to which this chunk belongs.
|
||||
* @param trigger The reason for this chunk being selected.
|
||||
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
||||
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
|
||||
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
|
||||
*/
|
||||
public MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, int trigger,
|
||||
long startTimeUs, long endTimeUs, int nextChunkIndex) {
|
||||
super(dataSource, dataSpec, format, trigger);
|
||||
this.startTimeUs = startTimeUs;
|
||||
this.endTimeUs = endTimeUs;
|
||||
this.nextChunkIndex = nextChunkIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this is the last chunk in the stream.
|
||||
*
|
||||
* @return True if this is the last chunk in the stream. False otherwise.
|
||||
*/
|
||||
public final boolean isLastChunk() {
|
||||
return nextChunkIndex == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to the beginning of the chunk.
|
||||
*/
|
||||
public final void seekToStart() {
|
||||
seekTo(startTimeUs, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to the specified position within the chunk.
|
||||
*
|
||||
* @param positionUs The desired seek time in microseconds.
|
||||
* @param allowNoop True if the seek is allowed to do nothing if the result is more accurate than
|
||||
* seeking to a key frame. Always pass false if it is required that the next sample be a key
|
||||
* frame.
|
||||
* @return True if the seek results in a discontinuity in the sequence of samples returned by
|
||||
* {@link #read(SampleHolder)}. False otherwise.
|
||||
*/
|
||||
public abstract boolean seekTo(long positionUs, boolean allowNoop);
|
||||
|
||||
/**
|
||||
* Reads the next media sample from the chunk.
|
||||
*
|
||||
* @param holder A holder to store the read sample.
|
||||
* @return True if a sample was read. False if more data is still required.
|
||||
* @throws ParserException If an error occurs parsing the media data.
|
||||
* @throws IllegalStateException If called before {@link #init}, or after {@link #release}
|
||||
*/
|
||||
public abstract boolean read(SampleHolder holder) throws ParserException;
|
||||
|
||||
/**
|
||||
* Returns the media format of the samples contained within this chunk.
|
||||
*
|
||||
* @return The sample media format.
|
||||
*/
|
||||
public abstract MediaFormat getMediaFormat();
|
||||
|
||||
/**
|
||||
* Returns the pssh information associated with the chunk.
|
||||
*
|
||||
* @return The pssh information.
|
||||
*/
|
||||
public abstract Map<UUID, byte[]> getPsshInfo();
|
||||
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.chunk;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* An Mp4 {@link MediaChunk}.
|
||||
*/
|
||||
public final class Mp4MediaChunk extends MediaChunk {
|
||||
|
||||
private final FragmentedMp4Extractor extractor;
|
||||
private final long sampleOffsetUs;
|
||||
|
||||
/**
|
||||
* @param dataSource A {@link DataSource} for loading the data.
|
||||
* @param dataSpec Defines the data to be loaded.
|
||||
* @param format The format of the stream to which this chunk belongs.
|
||||
* @param extractor The extractor that will be used to extract the samples.
|
||||
* @param trigger The reason for this chunk being selected.
|
||||
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
||||
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
|
||||
* @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor.
|
||||
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
|
||||
*/
|
||||
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
|
||||
int trigger, FragmentedMp4Extractor extractor, long startTimeUs, long endTimeUs,
|
||||
long sampleOffsetUs, int nextChunkIndex) {
|
||||
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
|
||||
this.extractor = extractor;
|
||||
this.sampleOffsetUs = sampleOffsetUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean seekTo(long positionUs, boolean allowNoop) {
|
||||
long seekTimeUs = positionUs + sampleOffsetUs;
|
||||
boolean isDiscontinuous = extractor.seekTo(seekTimeUs, allowNoop);
|
||||
if (isDiscontinuous) {
|
||||
resetReadPosition();
|
||||
}
|
||||
return isDiscontinuous;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean read(SampleHolder holder) throws ParserException {
|
||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||
Assertions.checkState(inputStream != null);
|
||||
int result = extractor.read(inputStream, holder);
|
||||
boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE_FULL) != 0;
|
||||
if (sampleRead) {
|
||||
holder.timeUs -= sampleOffsetUs;
|
||||
}
|
||||
return sampleRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaFormat getMediaFormat() {
|
||||
return extractor.getFormat();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<UUID, byte[]> getPsshInfo() {
|
||||
return extractor.getPsshInfo();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.chunk;
|
||||
|
||||
import com.google.android.exoplayer.ExoPlaybackException;
|
||||
import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.TrackInfo;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link ChunkSource} providing the ability to switch between multiple other {@link ChunkSource}
|
||||
* instances.
|
||||
*/
|
||||
public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
|
||||
|
||||
/**
|
||||
* A message to indicate a source selection. Source selection can only be performed when the
|
||||
* source is disabled.
|
||||
*/
|
||||
public static final int MSG_SELECT_TRACK = 1;
|
||||
|
||||
private final ChunkSource[] allSources;
|
||||
|
||||
private ChunkSource selectedSource;
|
||||
private boolean enabled;
|
||||
|
||||
public MultiTrackChunkSource(ChunkSource... sources) {
|
||||
this.allSources = sources;
|
||||
this.selectedSource = sources[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of tracks that this source can switch between. May be called safely from any
|
||||
* thread.
|
||||
*
|
||||
* @return The number of tracks.
|
||||
*/
|
||||
public int getTrackCount() {
|
||||
return allSources.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackInfo getTrackInfo() {
|
||||
return selectedSource.getTrackInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
selectedSource.enable();
|
||||
enabled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable(List<MediaChunk> queue) {
|
||||
selectedSource.disable(queue);
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void continueBuffering(long playbackPositionUs) {
|
||||
selectedSource.continueBuffering(playbackPositionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
|
||||
long playbackPositionUs, ChunkOperationHolder out) {
|
||||
selectedSource.getChunkOperation(queue, seekPositionUs, playbackPositionUs, out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IOException getError() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getMaxVideoDimensions(MediaFormat out) {
|
||||
selectedSource.getMaxVideoDimensions(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(int what, Object msg) throws ExoPlaybackException {
|
||||
Assertions.checkState(!enabled);
|
||||
if (what == MSG_SELECT_TRACK) {
|
||||
selectedSource = allSources[(Integer) msg];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.chunk;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A {@link MediaChunk} containing a single sample.
|
||||
*/
|
||||
public class SingleSampleMediaChunk extends MediaChunk {
|
||||
|
||||
/**
|
||||
* The sample header data. May be null.
|
||||
*/
|
||||
public final byte[] headerData;
|
||||
|
||||
private final MediaFormat sampleFormat;
|
||||
|
||||
/**
|
||||
* @param dataSource A {@link DataSource} for loading the data.
|
||||
* @param dataSpec Defines the data to be loaded.
|
||||
* @param format The format of the stream to which this chunk belongs.
|
||||
* @param trigger The reason for this chunk being selected.
|
||||
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
||||
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
|
||||
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
|
||||
* @param sampleFormat The format of the media contained by the chunk.
|
||||
*/
|
||||
public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
|
||||
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, MediaFormat sampleFormat) {
|
||||
this(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex,
|
||||
sampleFormat, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dataSource A {@link DataSource} for loading the data.
|
||||
* @param dataSpec Defines the data to be loaded.
|
||||
* @param format The format of the stream to which this chunk belongs.
|
||||
* @param trigger The reason for this chunk being selected.
|
||||
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
||||
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
|
||||
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
|
||||
* @param sampleFormat The format of the media contained by the chunk.
|
||||
* @param headerData Custom header data for the sample. May be null. If set, the header data is
|
||||
* prepended to the sample data returned when {@link #read(SampleHolder)} is called. It is
|
||||
* however not considered part of the loaded data, and so is not prepended to the data
|
||||
* returned by {@link #getLoadedData()}. It is also not reflected in the values returned by
|
||||
* {@link #bytesLoaded()} and {@link #getLength()}.
|
||||
*/
|
||||
public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
|
||||
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, MediaFormat sampleFormat,
|
||||
byte[] headerData) {
|
||||
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
|
||||
this.sampleFormat = sampleFormat;
|
||||
this.headerData = headerData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean read(SampleHolder holder) {
|
||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||
Assertions.checkState(inputStream != null);
|
||||
if (!isLoadFinished()) {
|
||||
return false;
|
||||
}
|
||||
int bytesLoaded = (int) bytesLoaded();
|
||||
int sampleSize = bytesLoaded;
|
||||
if (headerData != null) {
|
||||
sampleSize += headerData.length;
|
||||
}
|
||||
if (holder.allowDataBufferReplacement &&
|
||||
(holder.data == null || holder.data.capacity() < sampleSize)) {
|
||||
holder.data = ByteBuffer.allocate(sampleSize);
|
||||
}
|
||||
int bytesRead;
|
||||
if (holder.data != null) {
|
||||
if (headerData != null) {
|
||||
holder.data.put(headerData);
|
||||
}
|
||||
bytesRead = inputStream.read(holder.data, bytesLoaded);
|
||||
holder.size = sampleSize;
|
||||
} else {
|
||||
bytesRead = inputStream.skip(bytesLoaded);
|
||||
holder.size = 0;
|
||||
}
|
||||
Assertions.checkState(bytesRead == bytesLoaded);
|
||||
holder.timeUs = startTimeUs;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean seekTo(long positionUs, boolean allowNoop) {
|
||||
resetReadPosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaFormat getMediaFormat() {
|
||||
return sampleFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<UUID, byte[]> getPsshInfo() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.chunk;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.parser.webm.WebmExtractor;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A WebM {@link MediaChunk}.
|
||||
*/
|
||||
public final class WebmMediaChunk extends MediaChunk {
|
||||
|
||||
private final WebmExtractor extractor;
|
||||
|
||||
/**
|
||||
* @param dataSource A {@link DataSource} for loading the data.
|
||||
* @param dataSpec Defines the data to be loaded.
|
||||
* @param format The format of the stream to which this chunk belongs.
|
||||
* @param extractor The extractor that will be used to extract the samples.
|
||||
* @param trigger The reason for this chunk being selected.
|
||||
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
||||
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
|
||||
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
|
||||
*/
|
||||
public WebmMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
|
||||
int trigger, WebmExtractor extractor, long startTimeUs, long endTimeUs,
|
||||
int nextChunkIndex) {
|
||||
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
|
||||
this.extractor = extractor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean seekTo(long positionUs, boolean allowNoop) {
|
||||
boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop);
|
||||
if (isDiscontinuous) {
|
||||
resetReadPosition();
|
||||
}
|
||||
return isDiscontinuous;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean read(SampleHolder holder) {
|
||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||
Assertions.checkState(inputStream != null);
|
||||
return extractor.read(inputStream, holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaFormat getMediaFormat() {
|
||||
return extractor.getFormat();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<UUID, byte[]> getPsshInfo() {
|
||||
// TODO: Add support for Pssh to WebmExtractor
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,269 @@
|
||||
/*
|
||||
* 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.dash;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.TrackInfo;
|
||||
import com.google.android.exoplayer.chunk.Chunk;
|
||||
import com.google.android.exoplayer.chunk.ChunkOperationHolder;
|
||||
import com.google.android.exoplayer.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
|
||||
import com.google.android.exoplayer.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
|
||||
import com.google.android.exoplayer.dash.mpd.Representation;
|
||||
import com.google.android.exoplayer.parser.SegmentIndex;
|
||||
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An {@link ChunkSource} for Mp4 DASH streams.
|
||||
*/
|
||||
public class DashMp4ChunkSource implements ChunkSource {
|
||||
|
||||
public static final int DEFAULT_NUM_SEGMENTS_PER_CHUNK = 1;
|
||||
|
||||
private static final int EXPECTED_INITIALIZATION_RESULT =
|
||||
FragmentedMp4Extractor.RESULT_END_OF_STREAM
|
||||
| FragmentedMp4Extractor.RESULT_READ_MOOV
|
||||
| FragmentedMp4Extractor.RESULT_READ_SIDX;
|
||||
|
||||
private static final String TAG = "DashMp4ChunkSource";
|
||||
|
||||
private final TrackInfo trackInfo;
|
||||
private final DataSource dataSource;
|
||||
private final FormatEvaluator evaluator;
|
||||
private final Evaluation evaluation;
|
||||
private final int maxWidth;
|
||||
private final int maxHeight;
|
||||
private final int numSegmentsPerChunk;
|
||||
|
||||
private final Format[] formats;
|
||||
private final SparseArray<Representation> representations;
|
||||
private final SparseArray<FragmentedMp4Extractor> extractors;
|
||||
|
||||
private boolean lastChunkWasInitialization;
|
||||
|
||||
/**
|
||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||
* @param evaluator Selects from the available formats.
|
||||
* @param representations The representations to be considered by the source.
|
||||
*/
|
||||
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
||||
Representation... representations) {
|
||||
this(dataSource, evaluator, DEFAULT_NUM_SEGMENTS_PER_CHUNK, representations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||
* @param evaluator Selects from the available formats.
|
||||
* @param numSegmentsPerChunk The number of segments (as defined in the stream's segment index)
|
||||
* that should be grouped into a single chunk.
|
||||
* @param representations The representations to be considered by the source.
|
||||
*/
|
||||
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
||||
int numSegmentsPerChunk, Representation... representations) {
|
||||
this.dataSource = dataSource;
|
||||
this.evaluator = evaluator;
|
||||
this.numSegmentsPerChunk = numSegmentsPerChunk;
|
||||
this.formats = new Format[representations.length];
|
||||
this.extractors = new SparseArray<FragmentedMp4Extractor>();
|
||||
this.representations = new SparseArray<Representation>();
|
||||
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
|
||||
representations[0].periodDuration * 1000);
|
||||
this.evaluation = new Evaluation();
|
||||
int maxWidth = 0;
|
||||
int maxHeight = 0;
|
||||
for (int i = 0; i < representations.length; i++) {
|
||||
formats[i] = representations[i].format;
|
||||
maxWidth = Math.max(formats[i].width, maxWidth);
|
||||
maxHeight = Math.max(formats[i].height, maxHeight);
|
||||
extractors.append(formats[i].id, new FragmentedMp4Extractor());
|
||||
this.representations.put(formats[i].id, representations[i]);
|
||||
}
|
||||
this.maxWidth = maxWidth;
|
||||
this.maxHeight = maxHeight;
|
||||
Arrays.sort(formats, new DecreasingBandwidthComparator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void getMaxVideoDimensions(MediaFormat out) {
|
||||
if (trackInfo.mimeType.startsWith("video")) {
|
||||
out.setMaxVideoDimensions(maxWidth, maxHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final TrackInfo getTrackInfo() {
|
||||
return trackInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
evaluator.enable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable(List<MediaChunk> queue) {
|
||||
evaluator.disable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void continueBuffering(long playbackPositionUs) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
|
||||
long playbackPositionUs, ChunkOperationHolder out) {
|
||||
evaluation.queueSize = queue.size();
|
||||
if (evaluation.format == null || !lastChunkWasInitialization) {
|
||||
evaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
|
||||
}
|
||||
Format selectedFormat = evaluation.format;
|
||||
out.queueSize = evaluation.queueSize;
|
||||
|
||||
if (selectedFormat == null) {
|
||||
out.chunk = null;
|
||||
return;
|
||||
} else if (out.queueSize == queue.size() && out.chunk != null
|
||||
&& out.chunk.format.id == selectedFormat.id) {
|
||||
// We already have a chunk, and the evaluation hasn't changed either the format or the size
|
||||
// of the queue. Leave unchanged.
|
||||
return;
|
||||
}
|
||||
|
||||
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
||||
FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id);
|
||||
if (extractor.getTrack() == null) {
|
||||
Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor,
|
||||
dataSource, evaluation.trigger);
|
||||
lastChunkWasInitialization = true;
|
||||
out.chunk = initializationChunk;
|
||||
return;
|
||||
}
|
||||
|
||||
int nextIndex;
|
||||
if (queue.isEmpty()) {
|
||||
nextIndex = Arrays.binarySearch(extractor.getSegmentIndex().timesUs, seekPositionUs);
|
||||
nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex;
|
||||
} else {
|
||||
nextIndex = queue.get(out.queueSize - 1).nextChunkIndex;
|
||||
}
|
||||
|
||||
if (nextIndex == -1) {
|
||||
out.chunk = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource,
|
||||
extractor.getSegmentIndex(), nextIndex, evaluation.trigger, numSegmentsPerChunk);
|
||||
lastChunkWasInitialization = false;
|
||||
out.chunk = nextMediaChunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IOException getError() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Chunk newInitializationChunk(Representation representation,
|
||||
FragmentedMp4Extractor extractor, DataSource dataSource, int trigger) {
|
||||
DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1,
|
||||
representation.getCacheKey());
|
||||
return new InitializationMp4Loadable(dataSource, dataSpec, trigger, extractor, representation);
|
||||
}
|
||||
|
||||
private static Chunk newMediaChunk(Representation representation,
|
||||
FragmentedMp4Extractor extractor, DataSource dataSource, SegmentIndex sidx, int index,
|
||||
int trigger, int numSegmentsPerChunk) {
|
||||
|
||||
// Computes the segments to included in the next fetch.
|
||||
int numSegmentsToFetch = Math.min(numSegmentsPerChunk, sidx.length - index);
|
||||
int lastSegmentInChunk = index + numSegmentsToFetch - 1;
|
||||
int nextIndex = lastSegmentInChunk == sidx.length - 1 ? -1 : lastSegmentInChunk + 1;
|
||||
|
||||
long startTimeUs = sidx.timesUs[index];
|
||||
|
||||
// Compute the end time, prefer to use next segment start time if there is a next segment.
|
||||
long endTimeUs = nextIndex == -1 ?
|
||||
sidx.timesUs[lastSegmentInChunk] + sidx.durationsUs[lastSegmentInChunk] :
|
||||
sidx.timesUs[nextIndex];
|
||||
|
||||
long offset = (int) representation.indexEnd + 1 + sidx.offsets[index];
|
||||
|
||||
// Compute combined segments byte length.
|
||||
long size = 0;
|
||||
for (int i = index; i <= lastSegmentInChunk; i++) {
|
||||
size += sidx.sizes[i];
|
||||
}
|
||||
|
||||
DataSpec dataSpec = new DataSpec(representation.uri, offset, size,
|
||||
representation.getCacheKey());
|
||||
return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
|
||||
startTimeUs, endTimeUs, 0, nextIndex);
|
||||
}
|
||||
|
||||
private static class InitializationMp4Loadable extends Chunk {
|
||||
|
||||
private final Representation representation;
|
||||
private final FragmentedMp4Extractor extractor;
|
||||
|
||||
public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
||||
FragmentedMp4Extractor extractor, Representation representation) {
|
||||
super(dataSource, dataSpec, representation.format, trigger);
|
||||
this.extractor = extractor;
|
||||
this.representation = representation;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
|
||||
int result = extractor.read(stream, null);
|
||||
if (result != EXPECTED_INITIALIZATION_RESULT) {
|
||||
throw new ParserException("Invalid initialization data");
|
||||
}
|
||||
validateSegmentIndex(extractor.getSegmentIndex());
|
||||
}
|
||||
|
||||
private void validateSegmentIndex(SegmentIndex segmentIndex) {
|
||||
long expectedIndexLen = representation.indexEnd - representation.indexStart + 1;
|
||||
if (segmentIndex.sizeBytes != expectedIndexLen) {
|
||||
Log.w(TAG, "Sidx length mismatch: sidxLen = " + segmentIndex.sizeBytes +
|
||||
", ExpectedLen = " + expectedIndexLen);
|
||||
}
|
||||
long sidxContentLength = segmentIndex.offsets[segmentIndex.length - 1] +
|
||||
segmentIndex.sizes[segmentIndex.length - 1] + representation.indexEnd + 1;
|
||||
if (sidxContentLength != representation.contentLength) {
|
||||
Log.w(TAG, "ContentLength mismatch: Actual = " + sidxContentLength +
|
||||
", Expected = " + representation.contentLength);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
/*
|
||||
* 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.dash;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.TrackInfo;
|
||||
import com.google.android.exoplayer.chunk.Chunk;
|
||||
import com.google.android.exoplayer.chunk.ChunkOperationHolder;
|
||||
import com.google.android.exoplayer.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
|
||||
import com.google.android.exoplayer.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer.chunk.WebmMediaChunk;
|
||||
import com.google.android.exoplayer.dash.mpd.Representation;
|
||||
import com.google.android.exoplayer.parser.SegmentIndex;
|
||||
import com.google.android.exoplayer.parser.webm.WebmExtractor;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An {@link ChunkSource} for WebM DASH streams.
|
||||
*/
|
||||
public class DashWebmChunkSource implements ChunkSource {
|
||||
|
||||
private static final String TAG = "DashWebmChunkSource";
|
||||
|
||||
private final TrackInfo trackInfo;
|
||||
private final DataSource dataSource;
|
||||
private final FormatEvaluator evaluator;
|
||||
private final Evaluation evaluation;
|
||||
private final int maxWidth;
|
||||
private final int maxHeight;
|
||||
private final int numSegmentsPerChunk;
|
||||
|
||||
private final Format[] formats;
|
||||
private final SparseArray<Representation> representations;
|
||||
private final SparseArray<WebmExtractor> extractors;
|
||||
|
||||
private boolean lastChunkWasInitialization;
|
||||
|
||||
public DashWebmChunkSource(
|
||||
DataSource dataSource, FormatEvaluator evaluator, Representation... representations) {
|
||||
this(dataSource, evaluator, 1, representations);
|
||||
}
|
||||
|
||||
public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
||||
int numSegmentsPerChunk, Representation... representations) {
|
||||
this.dataSource = dataSource;
|
||||
this.evaluator = evaluator;
|
||||
this.numSegmentsPerChunk = numSegmentsPerChunk;
|
||||
this.formats = new Format[representations.length];
|
||||
this.extractors = new SparseArray<WebmExtractor>();
|
||||
this.representations = new SparseArray<Representation>();
|
||||
this.trackInfo = new TrackInfo(
|
||||
representations[0].format.mimeType, representations[0].periodDuration * 1000);
|
||||
this.evaluation = new Evaluation();
|
||||
int maxWidth = 0;
|
||||
int maxHeight = 0;
|
||||
for (int i = 0; i < representations.length; i++) {
|
||||
formats[i] = representations[i].format;
|
||||
maxWidth = Math.max(formats[i].width, maxWidth);
|
||||
maxHeight = Math.max(formats[i].height, maxHeight);
|
||||
extractors.append(formats[i].id, new WebmExtractor());
|
||||
this.representations.put(formats[i].id, representations[i]);
|
||||
}
|
||||
this.maxWidth = maxWidth;
|
||||
this.maxHeight = maxHeight;
|
||||
Arrays.sort(formats, new DecreasingBandwidthComparator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void getMaxVideoDimensions(MediaFormat out) {
|
||||
if (trackInfo.mimeType.startsWith("video")) {
|
||||
out.setMaxVideoDimensions(maxWidth, maxHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final TrackInfo getTrackInfo() {
|
||||
return trackInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
evaluator.enable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable(List<MediaChunk> queue) {
|
||||
evaluator.disable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void continueBuffering(long playbackPositionUs) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
|
||||
long playbackPositionUs, ChunkOperationHolder out) {
|
||||
evaluation.queueSize = queue.size();
|
||||
if (evaluation.format == null || !lastChunkWasInitialization) {
|
||||
evaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
|
||||
}
|
||||
Format selectedFormat = evaluation.format;
|
||||
out.queueSize = evaluation.queueSize;
|
||||
|
||||
if (selectedFormat == null) {
|
||||
out.chunk = null;
|
||||
return;
|
||||
} else if (out.queueSize == queue.size() && out.chunk != null
|
||||
&& out.chunk.format.id == selectedFormat.id) {
|
||||
// We already have a chunk, and the evaluation hasn't changed either the format or the size
|
||||
// of the queue. Leave unchanged.
|
||||
return;
|
||||
}
|
||||
|
||||
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
||||
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
|
||||
if (!extractor.isPrepared()) {
|
||||
Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor,
|
||||
dataSource, evaluation.trigger);
|
||||
lastChunkWasInitialization = true;
|
||||
out.chunk = initializationChunk;
|
||||
return;
|
||||
}
|
||||
|
||||
int nextIndex;
|
||||
if (queue.isEmpty()) {
|
||||
nextIndex = Arrays.binarySearch(extractor.getCues().timesUs, seekPositionUs);
|
||||
nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex;
|
||||
} else {
|
||||
nextIndex = queue.get(out.queueSize - 1).nextChunkIndex;
|
||||
}
|
||||
|
||||
if (nextIndex == -1) {
|
||||
out.chunk = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource,
|
||||
extractor.getCues(), nextIndex, evaluation.trigger, numSegmentsPerChunk);
|
||||
lastChunkWasInitialization = false;
|
||||
out.chunk = nextMediaChunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IOException getError() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Chunk newInitializationChunk(Representation representation,
|
||||
WebmExtractor extractor, DataSource dataSource, int trigger) {
|
||||
DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1,
|
||||
representation.getCacheKey());
|
||||
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, extractor, representation);
|
||||
}
|
||||
|
||||
private static Chunk newMediaChunk(Representation representation,
|
||||
WebmExtractor extractor, DataSource dataSource, SegmentIndex cues, int index,
|
||||
int trigger, int numSegmentsPerChunk) {
|
||||
|
||||
// Computes the segments to included in the next fetch.
|
||||
int numSegmentsToFetch = Math.min(numSegmentsPerChunk, cues.length - index);
|
||||
int lastSegmentInChunk = index + numSegmentsToFetch - 1;
|
||||
int nextIndex = lastSegmentInChunk == cues.length - 1 ? -1 : lastSegmentInChunk + 1;
|
||||
|
||||
long startTimeUs = cues.timesUs[index];
|
||||
|
||||
// Compute the end time, prefer to use next segment start time if there is a next segment.
|
||||
long endTimeUs = nextIndex == -1 ?
|
||||
cues.timesUs[lastSegmentInChunk] + cues.durationsUs[lastSegmentInChunk] :
|
||||
cues.timesUs[nextIndex];
|
||||
|
||||
long offset = cues.offsets[index];
|
||||
|
||||
// Compute combined segments byte length.
|
||||
long size = 0;
|
||||
for (int i = index; i <= lastSegmentInChunk; i++) {
|
||||
size += cues.sizes[i];
|
||||
}
|
||||
|
||||
DataSpec dataSpec = new DataSpec(representation.uri, offset, size,
|
||||
representation.getCacheKey());
|
||||
return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
|
||||
startTimeUs, endTimeUs, nextIndex);
|
||||
}
|
||||
|
||||
private static class InitializationWebmLoadable extends Chunk {
|
||||
|
||||
private final Representation representation;
|
||||
private final WebmExtractor extractor;
|
||||
|
||||
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
||||
WebmExtractor extractor, Representation representation) {
|
||||
super(dataSource, dataSpec, representation.format, trigger);
|
||||
this.extractor = extractor;
|
||||
this.representation = representation;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
|
||||
extractor.read(stream, null);
|
||||
if (!extractor.isPrepared()) {
|
||||
throw new ParserException("Invalid initialization data");
|
||||
}
|
||||
validateCues(extractor.getCues());
|
||||
}
|
||||
|
||||
private void validateCues(SegmentIndex cues) {
|
||||
long expectedSizeBytes = representation.indexEnd - representation.indexStart + 1;
|
||||
if (cues.sizeBytes != expectedSizeBytes) {
|
||||
Log.w(TAG, "Cues length mismatch: got " + cues.sizeBytes +
|
||||
" but expected " + expectedSizeBytes);
|
||||
}
|
||||
long expectedContentLength = cues.offsets[cues.length - 1] +
|
||||
cues.sizes[cues.length - 1] + representation.indexEnd + 1;
|
||||
if (representation.contentLength > 0
|
||||
&& expectedContentLength != representation.contentLength) {
|
||||
Log.w(TAG, "ContentLength mismatch: got " + expectedContentLength +
|
||||
" but expected " + representation.contentLength);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.dash.mpd;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a set of interchangeable encoded versions of a media content component.
|
||||
*/
|
||||
public final class AdaptationSet {
|
||||
|
||||
public static final int TYPE_UNKNOWN = -1;
|
||||
public static final int TYPE_VIDEO = 0;
|
||||
public static final int TYPE_AUDIO = 1;
|
||||
public static final int TYPE_TEXT = 2;
|
||||
|
||||
public final int id;
|
||||
|
||||
public final int type;
|
||||
|
||||
public final List<Representation> representations;
|
||||
public final List<ContentProtection> contentProtections;
|
||||
|
||||
public AdaptationSet(int id, int type, List<Representation> representations,
|
||||
List<ContentProtection> contentProtections) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.representations = Collections.unmodifiableList(representations);
|
||||
if (contentProtections == null) {
|
||||
this.contentProtections = Collections.emptyList();
|
||||
} else {
|
||||
this.contentProtections = Collections.unmodifiableList(contentProtections);
|
||||
}
|
||||
}
|
||||
|
||||
public AdaptationSet(int id, int type, List<Representation> representations) {
|
||||
this(id, type, representations, null);
|
||||
}
|
||||
|
||||
public boolean hasContentProtection() {
|
||||
return !contentProtections.isEmpty();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.dash.mpd;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a ContentProtection tag in an AdaptationSet. Holds arbitrary data for various DRM
|
||||
* schemes.
|
||||
*/
|
||||
public final class ContentProtection {
|
||||
|
||||
/**
|
||||
* Identifies the content protection scheme.
|
||||
*/
|
||||
public final String schemeUriId;
|
||||
/**
|
||||
* Protection scheme specific data.
|
||||
*/
|
||||
public final Map<String, String> keyedData;
|
||||
|
||||
/**
|
||||
* @param schemeUriId Identifies the content protection scheme.
|
||||
* @param keyedData Data specific to the scheme.
|
||||
*/
|
||||
public ContentProtection(String schemeUriId, Map<String, String> keyedData) {
|
||||
this.schemeUriId = schemeUriId;
|
||||
if (keyedData != null) {
|
||||
this.keyedData = Collections.unmodifiableMap(keyedData);
|
||||
} else {
|
||||
this.keyedData = Collections.emptyMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.dash.mpd;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a DASH media presentation description (mpd).
|
||||
*/
|
||||
public final class MediaPresentationDescription {
|
||||
|
||||
public final long duration;
|
||||
|
||||
public final long minBufferTime;
|
||||
|
||||
public final boolean dynamic;
|
||||
|
||||
public final long minUpdatePeriod;
|
||||
|
||||
public final List<Period> periods;
|
||||
|
||||
public MediaPresentationDescription(long duration, long minBufferTime, boolean dynamic,
|
||||
long minUpdatePeriod, List<Period> periods) {
|
||||
this.duration = duration;
|
||||
this.minBufferTime = minBufferTime;
|
||||
this.dynamic = dynamic;
|
||||
this.minUpdatePeriod = minUpdatePeriod;
|
||||
this.periods = Collections.unmodifiableList(periods);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.dash.mpd;
|
||||
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* A concrete implementation of {@link ManifestFetcher} for loading DASH manifests.
|
||||
* <p>
|
||||
* This class is provided for convenience, however it is expected that most applications will
|
||||
* contain their own mechanisms for making asynchronous network requests and parsing the response.
|
||||
* In such cases it is recommended that application developers use their existing solution rather
|
||||
* than this one.
|
||||
*/
|
||||
public final class MediaPresentationDescriptionFetcher extends
|
||||
ManifestFetcher<MediaPresentationDescription> {
|
||||
|
||||
private final MediaPresentationDescriptionParser parser;
|
||||
|
||||
/**
|
||||
* @param callback The callback to provide with the parsed manifest (or error).
|
||||
*/
|
||||
public MediaPresentationDescriptionFetcher(
|
||||
ManifestCallback<MediaPresentationDescription> callback) {
|
||||
super(callback);
|
||||
parser = new MediaPresentationDescriptionParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callback The callback to provide with the parsed manifest (or error).
|
||||
* @param timeoutMillis The timeout in milliseconds for the connection used to load the data.
|
||||
*/
|
||||
public MediaPresentationDescriptionFetcher(
|
||||
ManifestCallback<MediaPresentationDescription> callback, int timeoutMillis) {
|
||||
super(callback, timeoutMillis);
|
||||
parser = new MediaPresentationDescriptionParser();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
|
||||
String contentId) throws IOException, ParserException {
|
||||
try {
|
||||
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId);
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new ParserException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,353 @@
|
||||
/*
|
||||
* 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.dash.mpd;
|
||||
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A parser of media presentation description files.
|
||||
*/
|
||||
/*
|
||||
* TODO: Parse representation base attributes at multiple levels, and normalize the resulting
|
||||
* datastructure.
|
||||
* TODO: Decide how best to represent missing integer/double/long attributes.
|
||||
*/
|
||||
public class MediaPresentationDescriptionParser extends DefaultHandler {
|
||||
|
||||
private static final String TAG = "MediaPresentationDescriptionParser";
|
||||
|
||||
// Note: Does not support the date part of ISO 8601
|
||||
private static final Pattern DURATION =
|
||||
Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$");
|
||||
|
||||
private final XmlPullParserFactory xmlParserFactory;
|
||||
|
||||
public MediaPresentationDescriptionParser() {
|
||||
try {
|
||||
xmlParserFactory = XmlPullParserFactory.newInstance();
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a manifest from the provided {@link InputStream}.
|
||||
*
|
||||
* @param inputStream The stream from which to parse the manifest.
|
||||
* @param inputEncoding The encoding of the input.
|
||||
* @param contentId The content id of the media.
|
||||
* @return The parsed manifest.
|
||||
* @throws IOException If a problem occurred reading from the stream.
|
||||
* @throws XmlPullParserException If a problem occurred parsing the stream as xml.
|
||||
* @throws ParserException If a problem occurred parsing the xml as a DASH mpd.
|
||||
*/
|
||||
public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream,
|
||||
String inputEncoding, String contentId) throws XmlPullParserException, IOException,
|
||||
ParserException {
|
||||
XmlPullParser xpp = xmlParserFactory.newPullParser();
|
||||
xpp.setInput(inputStream, inputEncoding);
|
||||
int eventType = xpp.next();
|
||||
if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
|
||||
throw new ParserException(
|
||||
"inputStream does not contain a valid media presentation description");
|
||||
}
|
||||
return parseMediaPresentationDescription(xpp, contentId);
|
||||
}
|
||||
|
||||
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
|
||||
String contentId) throws XmlPullParserException, IOException {
|
||||
long duration = parseDurationMs(xpp, "mediaPresentationDuration");
|
||||
long minBufferTime = parseDurationMs(xpp, "minBufferTime");
|
||||
String typeString = xpp.getAttributeValue(null, "type");
|
||||
boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
|
||||
long minUpdateTime = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
|
||||
|
||||
List<Period> periods = new ArrayList<Period>();
|
||||
do {
|
||||
xpp.next();
|
||||
if (isStartTag(xpp, "Period")) {
|
||||
periods.add(parsePeriod(xpp, contentId, duration));
|
||||
}
|
||||
} while (!isEndTag(xpp, "MPD"));
|
||||
|
||||
return new MediaPresentationDescription(duration, minBufferTime, dynamic, minUpdateTime,
|
||||
periods);
|
||||
}
|
||||
|
||||
private Period parsePeriod(XmlPullParser xpp, String contentId, long mediaPresentationDuration)
|
||||
throws XmlPullParserException, IOException {
|
||||
int id = parseInt(xpp, "id");
|
||||
long start = parseDurationMs(xpp, "start", 0);
|
||||
long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration);
|
||||
|
||||
List<AdaptationSet> adaptationSets = new ArrayList<AdaptationSet>();
|
||||
List<Segment.Timeline> segmentTimelineList = null;
|
||||
int segmentStartNumber = 0;
|
||||
int segmentTimescale = 0;
|
||||
long presentationTimeOffset = 0;
|
||||
do {
|
||||
xpp.next();
|
||||
if (isStartTag(xpp, "AdaptationSet")) {
|
||||
adaptationSets.add(parseAdaptationSet(xpp, contentId, start, duration,
|
||||
segmentTimelineList));
|
||||
} else if (isStartTag(xpp, "SegmentList")) {
|
||||
segmentStartNumber = parseInt(xpp, "startNumber");
|
||||
segmentTimescale = parseInt(xpp, "timescale");
|
||||
presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", 0);
|
||||
segmentTimelineList = parsePeriodSegmentList(xpp, segmentStartNumber);
|
||||
}
|
||||
} while (!isEndTag(xpp, "Period"));
|
||||
|
||||
return new Period(id, start, duration, adaptationSets, segmentTimelineList,
|
||||
segmentStartNumber, segmentTimescale, presentationTimeOffset);
|
||||
}
|
||||
|
||||
private List<Segment.Timeline> parsePeriodSegmentList(
|
||||
XmlPullParser xpp, long segmentStartNumber) throws XmlPullParserException, IOException {
|
||||
List<Segment.Timeline> segmentTimelineList = new ArrayList<Segment.Timeline>();
|
||||
|
||||
do {
|
||||
xpp.next();
|
||||
if (isStartTag(xpp, "SegmentTimeline")) {
|
||||
do {
|
||||
xpp.next();
|
||||
if (isStartTag(xpp, "S")) {
|
||||
long duration = parseLong(xpp, "d");
|
||||
segmentTimelineList.add(new Segment.Timeline(segmentStartNumber, duration));
|
||||
segmentStartNumber++;
|
||||
}
|
||||
} while (!isEndTag(xpp, "SegmentTimeline"));
|
||||
}
|
||||
} while (!isEndTag(xpp, "SegmentList"));
|
||||
|
||||
return segmentTimelineList;
|
||||
}
|
||||
|
||||
private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, long periodStart,
|
||||
long periodDuration, List<Segment.Timeline> segmentTimelineList)
|
||||
throws XmlPullParserException, IOException {
|
||||
int id = -1;
|
||||
int contentType = AdaptationSet.TYPE_UNKNOWN;
|
||||
|
||||
// TODO: Correctly handle other common attributes and elements. See 23009-1 Table 9.
|
||||
String mimeType = xpp.getAttributeValue(null, "mimeType");
|
||||
if (mimeType != null) {
|
||||
if (MimeTypes.isAudio(mimeType)) {
|
||||
contentType = AdaptationSet.TYPE_AUDIO;
|
||||
} else if (MimeTypes.isVideo(mimeType)) {
|
||||
contentType = AdaptationSet.TYPE_VIDEO;
|
||||
} else if (MimeTypes.isText(mimeType)
|
||||
|| mimeType.equalsIgnoreCase(MimeTypes.APPLICATION_TTML)) {
|
||||
contentType = AdaptationSet.TYPE_TEXT;
|
||||
}
|
||||
}
|
||||
|
||||
List<ContentProtection> contentProtections = null;
|
||||
List<Representation> representations = new ArrayList<Representation>();
|
||||
do {
|
||||
xpp.next();
|
||||
if (contentType != AdaptationSet.TYPE_UNKNOWN) {
|
||||
if (isStartTag(xpp, "ContentProtection")) {
|
||||
if (contentProtections == null) {
|
||||
contentProtections = new ArrayList<ContentProtection>();
|
||||
}
|
||||
contentProtections.add(parseContentProtection(xpp));
|
||||
} else if (isStartTag(xpp, "ContentComponent")) {
|
||||
id = Integer.parseInt(xpp.getAttributeValue(null, "id"));
|
||||
String contentTypeString = xpp.getAttributeValue(null, "contentType");
|
||||
contentType = "video".equals(contentTypeString) ? AdaptationSet.TYPE_VIDEO
|
||||
: "audio".equals(contentTypeString) ? AdaptationSet.TYPE_AUDIO
|
||||
: AdaptationSet.TYPE_UNKNOWN;
|
||||
} else if (isStartTag(xpp, "Representation")) {
|
||||
representations.add(parseRepresentation(xpp, contentId, periodStart, periodDuration,
|
||||
mimeType, segmentTimelineList));
|
||||
}
|
||||
}
|
||||
} while (!isEndTag(xpp, "AdaptationSet"));
|
||||
|
||||
return new AdaptationSet(id, contentType, representations, contentProtections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a ContentProtection element.
|
||||
*
|
||||
* @throws XmlPullParserException If an error occurs parsing the element.
|
||||
* @throws IOException If an error occurs reading the element.
|
||||
**/
|
||||
protected ContentProtection parseContentProtection(XmlPullParser xpp)
|
||||
throws XmlPullParserException, IOException {
|
||||
String schemeUriId = xpp.getAttributeValue(null, "schemeUriId");
|
||||
return new ContentProtection(schemeUriId, null);
|
||||
}
|
||||
|
||||
private Representation parseRepresentation(XmlPullParser xpp, String contentId, long periodStart,
|
||||
long periodDuration, String parentMimeType, List<Segment.Timeline> segmentTimelineList)
|
||||
throws XmlPullParserException, IOException {
|
||||
int id;
|
||||
try {
|
||||
id = parseInt(xpp, "id");
|
||||
} catch (NumberFormatException nfe) {
|
||||
Log.d(TAG, "Unable to parse id; " + nfe.getMessage());
|
||||
// TODO: need a way to generate a unique and stable id; use hashCode for now
|
||||
id = xpp.getAttributeValue(null, "id").hashCode();
|
||||
}
|
||||
int bandwidth = parseInt(xpp, "bandwidth") / 8;
|
||||
int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
|
||||
int width = parseInt(xpp, "width");
|
||||
int height = parseInt(xpp, "height");
|
||||
|
||||
String mimeType = xpp.getAttributeValue(null, "mimeType");
|
||||
if (mimeType == null) {
|
||||
mimeType = parentMimeType;
|
||||
}
|
||||
|
||||
String representationUrl = null;
|
||||
long indexStart = -1;
|
||||
long indexEnd = -1;
|
||||
long initializationStart = -1;
|
||||
long initializationEnd = -1;
|
||||
int numChannels = -1;
|
||||
List<Segment> segmentList = null;
|
||||
do {
|
||||
xpp.next();
|
||||
if (isStartTag(xpp, "BaseURL")) {
|
||||
xpp.next();
|
||||
representationUrl = xpp.getText();
|
||||
} else if (isStartTag(xpp, "AudioChannelConfiguration")) {
|
||||
numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value"));
|
||||
} else if (isStartTag(xpp, "SegmentBase")) {
|
||||
String[] indexRange = xpp.getAttributeValue(null, "indexRange").split("-");
|
||||
indexStart = Long.parseLong(indexRange[0]);
|
||||
indexEnd = Long.parseLong(indexRange[1]);
|
||||
} else if (isStartTag(xpp, "SegmentList")) {
|
||||
segmentList = parseRepresentationSegmentList(xpp, segmentTimelineList);
|
||||
} else if (isStartTag(xpp, "Initialization")) {
|
||||
String[] indexRange = xpp.getAttributeValue(null, "range").split("-");
|
||||
initializationStart = Long.parseLong(indexRange[0]);
|
||||
initializationEnd = Long.parseLong(indexRange[1]);
|
||||
}
|
||||
} while (!isEndTag(xpp, "Representation"));
|
||||
|
||||
Uri uri = Uri.parse(representationUrl);
|
||||
Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate,
|
||||
bandwidth);
|
||||
if (segmentList == null) {
|
||||
return new Representation(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED,
|
||||
initializationStart, initializationEnd, indexStart, indexEnd, periodStart,
|
||||
periodDuration);
|
||||
} else {
|
||||
return new SegmentedRepresentation(contentId, format, uri, initializationStart,
|
||||
initializationEnd, indexStart, indexEnd, periodStart, periodDuration, segmentList);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Segment> parseRepresentationSegmentList(XmlPullParser xpp,
|
||||
List<Segment.Timeline> segmentTimelineList) throws XmlPullParserException, IOException {
|
||||
List<Segment> segmentList = new ArrayList<Segment>();
|
||||
int i = 0;
|
||||
|
||||
do {
|
||||
xpp.next();
|
||||
if (isStartTag(xpp, "Initialization")) {
|
||||
String url = xpp.getAttributeValue(null, "sourceURL");
|
||||
String[] indexRange = xpp.getAttributeValue(null, "range").split("-");
|
||||
long initializationStart = Long.parseLong(indexRange[0]);
|
||||
long initializationEnd = Long.parseLong(indexRange[1]);
|
||||
segmentList.add(new Segment.Initialization(url, initializationStart, initializationEnd));
|
||||
} else if (isStartTag(xpp, "SegmentURL")) {
|
||||
String url = xpp.getAttributeValue(null, "media");
|
||||
String mediaRange = xpp.getAttributeValue(null, "mediaRange");
|
||||
long sequenceNumber = segmentTimelineList.get(i).sequenceNumber;
|
||||
long duration = segmentTimelineList.get(i).duration;
|
||||
i++;
|
||||
if (mediaRange != null) {
|
||||
String[] mediaRangeArray = xpp.getAttributeValue(null, "mediaRange").split("-");
|
||||
long mediaStart = Long.parseLong(mediaRangeArray[0]);
|
||||
segmentList.add(new Segment.Media(url, mediaStart, sequenceNumber, duration));
|
||||
} else {
|
||||
segmentList.add(new Segment.Media(url, sequenceNumber, duration));
|
||||
}
|
||||
}
|
||||
} while (!isEndTag(xpp, "SegmentList"));
|
||||
|
||||
return segmentList;
|
||||
}
|
||||
|
||||
protected static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
|
||||
return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName());
|
||||
}
|
||||
|
||||
protected static boolean isStartTag(XmlPullParser xpp, String name)
|
||||
throws XmlPullParserException {
|
||||
return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName());
|
||||
}
|
||||
|
||||
protected static int parseInt(XmlPullParser xpp, String name) {
|
||||
String value = xpp.getAttributeValue(null, name);
|
||||
return value == null ? -1 : Integer.parseInt(value);
|
||||
}
|
||||
|
||||
protected static long parseLong(XmlPullParser xpp, String name) {
|
||||
return parseLong(xpp, name, -1);
|
||||
}
|
||||
|
||||
protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) {
|
||||
String value = xpp.getAttributeValue(null, name);
|
||||
return value == null ? defaultValue : Long.parseLong(value);
|
||||
}
|
||||
|
||||
private long parseDurationMs(XmlPullParser xpp, String name) {
|
||||
return parseDurationMs(xpp, name, -1);
|
||||
}
|
||||
|
||||
private long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) {
|
||||
String value = xpp.getAttributeValue(null, name);
|
||||
if (value != null) {
|
||||
Matcher matcher = DURATION.matcher(value);
|
||||
if (matcher.matches()) {
|
||||
String hours = matcher.group(2);
|
||||
double durationSeconds = (hours != null) ? Double.parseDouble(hours) * 3600 : 0;
|
||||
String minutes = matcher.group(4);
|
||||
durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0;
|
||||
String seconds = matcher.group(6);
|
||||
durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0;
|
||||
return (long) (durationSeconds * 1000);
|
||||
} else {
|
||||
return (long) (Double.parseDouble(value) * 3600 * 1000);
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.dash.mpd;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Encapsulates media content components over a contiguous period of time.
|
||||
*/
|
||||
public final class Period {
|
||||
|
||||
public final int id;
|
||||
|
||||
public final long start;
|
||||
|
||||
public final long duration;
|
||||
|
||||
public final List<AdaptationSet> adaptationSets;
|
||||
|
||||
public final List<Segment.Timeline> segmentList;
|
||||
|
||||
public final int segmentStartNumber;
|
||||
|
||||
public final int segmentTimescale;
|
||||
|
||||
public final long presentationTimeOffset;
|
||||
|
||||
public Period(int id, long start, long duration, List<AdaptationSet> adaptationSets) {
|
||||
this(id, start, duration, adaptationSets, null, 0, 0, 0);
|
||||
}
|
||||
|
||||
public Period(int id, long start, long duration, List<AdaptationSet> adaptationSets,
|
||||
List<Segment.Timeline> segmentList, int segmentStartNumber, int segmentTimescale) {
|
||||
this(id, start, duration, adaptationSets, segmentList, segmentStartNumber, segmentTimescale, 0);
|
||||
}
|
||||
|
||||
public Period(int id, long start, long duration, List<AdaptationSet> adaptationSets,
|
||||
List<Segment.Timeline> segmentList, int segmentStartNumber, int segmentTimescale,
|
||||
long presentationTimeOffset) {
|
||||
this.id = id;
|
||||
this.start = start;
|
||||
this.duration = duration;
|
||||
this.adaptationSets = Collections.unmodifiableList(adaptationSets);
|
||||
if (segmentList != null) {
|
||||
this.segmentList = Collections.unmodifiableList(segmentList);
|
||||
} else {
|
||||
this.segmentList = null;
|
||||
}
|
||||
this.segmentStartNumber = segmentStartNumber;
|
||||
this.segmentTimescale = segmentTimescale;
|
||||
this.presentationTimeOffset = presentationTimeOffset;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.dash.mpd;
|
||||
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* A flat version of a DASH representation.
|
||||
*/
|
||||
public class Representation {
|
||||
|
||||
/**
|
||||
* Identifies the piece of content to which this {@link Representation} belongs.
|
||||
* <p>
|
||||
* For example, all {@link Representation}s belonging to a video should have the same
|
||||
* {@link #contentId}, which should uniquely identify that video.
|
||||
*/
|
||||
public final String contentId;
|
||||
|
||||
/**
|
||||
* Identifies the revision of the {@link Representation}.
|
||||
* <p>
|
||||
* If the media for a given ({@link #contentId} can change over time without a change to the
|
||||
* {@link #format}'s {@link Format#id} (e.g. as a result of re-encoding the media with an
|
||||
* updated encoder), then this identifier must uniquely identify the revision of the media. The
|
||||
* timestamp at which the media was encoded is often a suitable.
|
||||
*/
|
||||
public final long revisionId;
|
||||
|
||||
/**
|
||||
* The format in which the {@link Representation} is encoded.
|
||||
*/
|
||||
public final Format format;
|
||||
|
||||
public final long contentLength;
|
||||
|
||||
public final long initializationStart;
|
||||
|
||||
public final long initializationEnd;
|
||||
|
||||
public final long indexStart;
|
||||
|
||||
public final long indexEnd;
|
||||
|
||||
public final long periodStart;
|
||||
|
||||
public final long periodDuration;
|
||||
|
||||
public final Uri uri;
|
||||
|
||||
public Representation(String contentId, long revisionId, Format format, Uri uri,
|
||||
long contentLength, long initializationStart, long initializationEnd, long indexStart,
|
||||
long indexEnd, long periodStart, long periodDuration) {
|
||||
this.contentId = contentId;
|
||||
this.revisionId = revisionId;
|
||||
this.format = format;
|
||||
this.contentLength = contentLength;
|
||||
this.initializationStart = initializationStart;
|
||||
this.initializationEnd = initializationEnd;
|
||||
this.indexStart = indexStart;
|
||||
this.indexEnd = indexEnd;
|
||||
this.periodStart = periodStart;
|
||||
this.periodDuration = periodDuration;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cache key for the {@link Representation}, in the format
|
||||
* {@link #contentId}.{@link #format.id}.{@link #revisionId}.
|
||||
*
|
||||
* @return A cache key.
|
||||
*/
|
||||
public String getCacheKey() {
|
||||
return contentId + "." + format.id + "." + revisionId;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.dash.mpd;
|
||||
|
||||
/**
|
||||
* Represents a particular segment in a Representation.
|
||||
*
|
||||
*/
|
||||
public abstract class Segment {
|
||||
|
||||
public final String relativeUri;
|
||||
|
||||
public final long sequenceNumber;
|
||||
|
||||
public final long duration;
|
||||
|
||||
public Segment(String relativeUri, long sequenceNumber, long duration) {
|
||||
this.relativeUri = relativeUri;
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a timeline segment from the MPD's SegmentTimeline list.
|
||||
*/
|
||||
public static class Timeline extends Segment {
|
||||
|
||||
public Timeline(long sequenceNumber, long duration) {
|
||||
super(null, sequenceNumber, duration);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an initialization segment.
|
||||
*/
|
||||
public static class Initialization extends Segment {
|
||||
|
||||
public final long initializationStart;
|
||||
public final long initializationEnd;
|
||||
|
||||
public Initialization(String relativeUri, long initializationStart,
|
||||
long initializationEnd) {
|
||||
super(relativeUri, -1, -1);
|
||||
this.initializationStart = initializationStart;
|
||||
this.initializationEnd = initializationEnd;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a media segment.
|
||||
*/
|
||||
public static class Media extends Segment {
|
||||
|
||||
public final long mediaStart;
|
||||
|
||||
public Media(String relativeUri, long sequenceNumber, long duration) {
|
||||
this(relativeUri, 0, sequenceNumber, duration);
|
||||
}
|
||||
|
||||
public Media(String uri, long mediaStart, long sequenceNumber, long duration) {
|
||||
super(uri, sequenceNumber, duration);
|
||||
this.mediaStart = mediaStart;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.dash.mpd;
|
||||
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a DASH Representation which uses the SegmentList structure (i.e. it has a list of
|
||||
* Segment URLs instead of a single URL).
|
||||
*/
|
||||
public class SegmentedRepresentation extends Representation {
|
||||
|
||||
private List<Segment> segmentList;
|
||||
|
||||
public SegmentedRepresentation(String contentId, Format format, Uri uri, long initializationStart,
|
||||
long initializationEnd, long indexStart, long indexEnd, long periodStart, long periodDuration,
|
||||
List<Segment> segmentList) {
|
||||
super(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED, initializationStart,
|
||||
initializationEnd, indexStart, indexEnd, periodStart, periodDuration);
|
||||
this.segmentList = segmentList;
|
||||
}
|
||||
|
||||
public int getNumSegments() {
|
||||
return segmentList.size();
|
||||
}
|
||||
|
||||
public Segment getSegment(int i) {
|
||||
return segmentList.get(i);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.drm;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaCrypto;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Manages a DRM session.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public interface DrmSessionManager {
|
||||
|
||||
/**
|
||||
* The error state. {@link #getError()} can be used to retrieve the cause.
|
||||
*/
|
||||
public static final int STATE_ERROR = 0;
|
||||
/**
|
||||
* The session is closed.
|
||||
*/
|
||||
public static final int STATE_CLOSED = 1;
|
||||
/**
|
||||
* The session is being opened (i.e. {@link #open(Map, String)} has been called, but the session
|
||||
* is not yet open).
|
||||
*/
|
||||
public static final int STATE_OPENING = 2;
|
||||
/**
|
||||
* The session is open, but does not yet have the keys required for decryption.
|
||||
*/
|
||||
public static final int STATE_OPENED = 3;
|
||||
/**
|
||||
* The session is open and has the keys required for decryption.
|
||||
*/
|
||||
public static final int STATE_OPENED_WITH_KEYS = 4;
|
||||
|
||||
/**
|
||||
* Opens the session, possibly asynchronously.
|
||||
*
|
||||
* @param drmInitData Initialization data for the drm schemes supported by the media, keyed by
|
||||
* scheme UUID.
|
||||
* @param mimeType The mimeType of the media.
|
||||
*/
|
||||
void open(Map<UUID, byte[]> drmInitData, String mimeType);
|
||||
|
||||
/**
|
||||
* Closes the session.
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Gets the current state of the session.
|
||||
*
|
||||
* @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
|
||||
* {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
|
||||
*/
|
||||
int getState();
|
||||
|
||||
/**
|
||||
* Gets a {@link MediaCrypto} for the open session.
|
||||
* <p>
|
||||
* This method may be called when the manager is in the following states:
|
||||
* {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
|
||||
*
|
||||
* @return A {@link MediaCrypto} for the open session.
|
||||
* @throws IllegalStateException If called when a session isn't opened.
|
||||
*/
|
||||
MediaCrypto getMediaCrypto();
|
||||
|
||||
/**
|
||||
* Whether the session requires a secure decoder for the specified mime type.
|
||||
* <p>
|
||||
* Normally this method should return {@link MediaCrypto#requiresSecureDecoderComponent(String)},
|
||||
* however in some cases implementations may wish to modify the return value (i.e. to force a
|
||||
* secure decoder even when one is not required).
|
||||
* <p>
|
||||
* This method may be called when the manager is in the following states:
|
||||
* {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
|
||||
*
|
||||
* @return Whether the open session requires a secure decoder for the specified mime type.
|
||||
* @throws IllegalStateException If called when a session isn't opened.
|
||||
*/
|
||||
boolean requiresSecureDecoderComponent(String mimeType);
|
||||
|
||||
/**
|
||||
* Gets the cause of the error state.
|
||||
* <p>
|
||||
* This method may be called when the manager is in any state.
|
||||
*
|
||||
* @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
|
||||
*/
|
||||
Exception getError();
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.drm;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaDrm;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Performs {@link MediaDrm} key and provisioning requests.
|
||||
*/
|
||||
@TargetApi(18)
|
||||
public interface MediaDrmCallback {
|
||||
|
||||
/**
|
||||
* Executes a provisioning request.
|
||||
*
|
||||
* @param uuid The UUID of the content protection scheme.
|
||||
* @param request The request.
|
||||
* @return The response data.
|
||||
* @throws Exception If an error occurred executing the request.
|
||||
*/
|
||||
byte[] executeProvisionRequest(UUID uuid, MediaDrm.ProvisionRequest request) throws Exception;
|
||||
|
||||
/**
|
||||
* Executes a key request.
|
||||
*
|
||||
* @param uuid The UUID of the content protection scheme.
|
||||
* @param request The request.
|
||||
* @return The response data.
|
||||
* @throws Exception If an error occurred executing the request.
|
||||
*/
|
||||
byte[] executeKeyRequest(UUID uuid, MediaDrm.KeyRequest request) throws Exception;
|
||||
|
||||
}
|
@ -0,0 +1,383 @@
|
||||
/*
|
||||
* 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.drm;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.DeniedByServerException;
|
||||
import android.media.MediaCrypto;
|
||||
import android.media.MediaDrm;
|
||||
import android.media.MediaDrm.KeyRequest;
|
||||
import android.media.MediaDrm.OnEventListener;
|
||||
import android.media.MediaDrm.ProvisionRequest;
|
||||
import android.media.NotProvisionedException;
|
||||
import android.media.UnsupportedSchemeException;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A base class for {@link DrmSessionManager} implementations that support streaming playbacks
|
||||
* using {@link MediaDrm}.
|
||||
*/
|
||||
@TargetApi(18)
|
||||
public class StreamingDrmSessionManager implements DrmSessionManager {
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be notified of {@link StreamingDrmSessionManager}
|
||||
* events.
|
||||
*/
|
||||
public interface EventListener {
|
||||
|
||||
/**
|
||||
* Invoked when a drm error occurs.
|
||||
*
|
||||
* @param e The corresponding exception.
|
||||
*/
|
||||
void onDrmSessionManagerError(Exception e);
|
||||
|
||||
}
|
||||
|
||||
private static final int MSG_PROVISION = 0;
|
||||
private static final int MSG_KEYS = 1;
|
||||
|
||||
private final Handler eventHandler;
|
||||
private final EventListener eventListener;
|
||||
private final MediaDrm mediaDrm;
|
||||
|
||||
/* package */ final MediaDrmHandler mediaDrmHandler;
|
||||
/* package */ final MediaDrmCallback callback;
|
||||
/* package */ final PostResponseHandler postResponseHandler;
|
||||
/* package */ final UUID uuid;
|
||||
|
||||
private HandlerThread requestHandlerThread;
|
||||
private Handler postRequestHandler;
|
||||
|
||||
private int openCount;
|
||||
private int state;
|
||||
private MediaCrypto mediaCrypto;
|
||||
private Exception lastException;
|
||||
private String mimeType;
|
||||
private byte[] schemePsshData;
|
||||
private byte[] sessionId;
|
||||
|
||||
/**
|
||||
* @param uuid The UUID of the drm scheme.
|
||||
* @param playbackLooper The looper associated with the media playback thread. Should usually be
|
||||
* obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}.
|
||||
* @param callback Performs key and provisioning requests.
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @throws UnsupportedSchemeException If the specified DRM scheme is not supported.
|
||||
*/
|
||||
public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback,
|
||||
Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException {
|
||||
this.uuid = uuid;
|
||||
this.callback = callback;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
mediaDrm = new MediaDrm(uuid);
|
||||
mediaDrm.setOnEventListener(new MediaDrmEventListener());
|
||||
mediaDrmHandler = new MediaDrmHandler(playbackLooper);
|
||||
postResponseHandler = new PostResponseHandler(playbackLooper);
|
||||
state = STATE_CLOSED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaCrypto getMediaCrypto() {
|
||||
if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return mediaCrypto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresSecureDecoderComponent(String mimeType) {
|
||||
if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return mediaCrypto.requiresSecureDecoderComponent(mimeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Exception getError() {
|
||||
return state == STATE_ERROR ? lastException : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides access to {@link MediaDrm#getPropertyString(String)}.
|
||||
* <p>
|
||||
* This method may be called when the manager is in any state.
|
||||
*
|
||||
* @param key The key to request.
|
||||
* @return The retrieved property.
|
||||
*/
|
||||
public final String getPropertyString(String key) {
|
||||
return mediaDrm.getPropertyString(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides access to {@link MediaDrm#getPropertyByteArray(String)}.
|
||||
* <p>
|
||||
* This method may be called when the manager is in any state.
|
||||
*
|
||||
* @param key The key to request.
|
||||
* @return The retrieved property.
|
||||
*/
|
||||
public final byte[] getPropertyByteArray(String key) {
|
||||
return mediaDrm.getPropertyByteArray(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open(Map<UUID, byte[]> psshData, String mimeType) {
|
||||
if (++openCount != 1) {
|
||||
return;
|
||||
}
|
||||
if (postRequestHandler == null) {
|
||||
requestHandlerThread = new HandlerThread("DrmRequestHandler");
|
||||
requestHandlerThread.start();
|
||||
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
|
||||
}
|
||||
if (this.schemePsshData == null) {
|
||||
this.mimeType = mimeType;
|
||||
schemePsshData = psshData.get(uuid);
|
||||
if (schemePsshData == null) {
|
||||
onError(new IllegalStateException("Media does not support uuid: " + uuid));
|
||||
return;
|
||||
}
|
||||
}
|
||||
state = STATE_OPENING;
|
||||
openInternal(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (--openCount != 0) {
|
||||
return;
|
||||
}
|
||||
state = STATE_CLOSED;
|
||||
mediaDrmHandler.removeCallbacksAndMessages(null);
|
||||
postResponseHandler.removeCallbacksAndMessages(null);
|
||||
postRequestHandler.removeCallbacksAndMessages(null);
|
||||
postRequestHandler = null;
|
||||
requestHandlerThread.quit();
|
||||
requestHandlerThread = null;
|
||||
schemePsshData = null;
|
||||
mediaCrypto = null;
|
||||
lastException = null;
|
||||
if (sessionId != null) {
|
||||
mediaDrm.closeSession(sessionId);
|
||||
sessionId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void openInternal(boolean allowProvisioning) {
|
||||
try {
|
||||
sessionId = mediaDrm.openSession();
|
||||
mediaCrypto = new MediaCrypto(uuid, sessionId);
|
||||
state = STATE_OPENED;
|
||||
postKeyRequest();
|
||||
} catch (NotProvisionedException e) {
|
||||
if (allowProvisioning) {
|
||||
postProvisionRequest();
|
||||
} else {
|
||||
onError(e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void postProvisionRequest() {
|
||||
ProvisionRequest request = mediaDrm.getProvisionRequest();
|
||||
postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget();
|
||||
}
|
||||
|
||||
private void onProvisionResponse(Object response) {
|
||||
if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
|
||||
// This event is stale.
|
||||
return;
|
||||
}
|
||||
|
||||
if (response instanceof Exception) {
|
||||
onError((Exception) response);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mediaDrm.provideProvisionResponse((byte[]) response);
|
||||
if (state == STATE_OPENING) {
|
||||
openInternal(false);
|
||||
} else {
|
||||
postKeyRequest();
|
||||
}
|
||||
} catch (DeniedByServerException e) {
|
||||
onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void postKeyRequest() {
|
||||
KeyRequest keyRequest;
|
||||
try {
|
||||
keyRequest = mediaDrm.getKeyRequest(sessionId, schemePsshData, mimeType,
|
||||
MediaDrm.KEY_TYPE_STREAMING, null);
|
||||
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
|
||||
} catch (NotProvisionedException e) {
|
||||
onKeysError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onKeyResponse(Object response) {
|
||||
if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
|
||||
// This event is stale.
|
||||
return;
|
||||
}
|
||||
|
||||
if (response instanceof Exception) {
|
||||
onKeysError((Exception) response);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
|
||||
state = STATE_OPENED_WITH_KEYS;
|
||||
} catch (Exception e) {
|
||||
onKeysError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onKeysError(Exception e) {
|
||||
if (e instanceof NotProvisionedException) {
|
||||
postProvisionRequest();
|
||||
} else {
|
||||
onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onError(Exception e) {
|
||||
lastException = e;
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onDrmSessionManagerError(lastException);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (state != STATE_OPENED_WITH_KEYS) {
|
||||
state = STATE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("HandlerLeak")
|
||||
private class MediaDrmHandler extends Handler {
|
||||
|
||||
public MediaDrmHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (openCount == 0 || (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)) {
|
||||
return;
|
||||
}
|
||||
switch (msg.what) {
|
||||
case MediaDrm.EVENT_KEY_REQUIRED:
|
||||
postKeyRequest();
|
||||
return;
|
||||
case MediaDrm.EVENT_KEY_EXPIRED:
|
||||
state = STATE_OPENED;
|
||||
postKeyRequest();
|
||||
return;
|
||||
case MediaDrm.EVENT_PROVISION_REQUIRED:
|
||||
state = STATE_OPENED;
|
||||
postProvisionRequest();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class MediaDrmEventListener implements OnEventListener {
|
||||
|
||||
@Override
|
||||
public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) {
|
||||
mediaDrmHandler.sendEmptyMessage(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("HandlerLeak")
|
||||
private class PostResponseHandler extends Handler {
|
||||
|
||||
public PostResponseHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_PROVISION:
|
||||
onProvisionResponse(msg.obj);
|
||||
return;
|
||||
case MSG_KEYS:
|
||||
onKeyResponse(msg.obj);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("HandlerLeak")
|
||||
private class PostRequestHandler extends Handler {
|
||||
|
||||
public PostRequestHandler(Looper backgroundLooper) {
|
||||
super(backgroundLooper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
Object response;
|
||||
try {
|
||||
switch (msg.what) {
|
||||
case MSG_PROVISION:
|
||||
response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj);
|
||||
break;
|
||||
case MSG_KEYS:
|
||||
response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
response = e;
|
||||
}
|
||||
postResponseHandler.obtainMessage(msg.what, response).sendToTarget();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.parser;
|
||||
|
||||
/**
|
||||
* Defines segments within a media stream.
|
||||
*/
|
||||
public final class SegmentIndex {
|
||||
|
||||
/**
|
||||
* The size in bytes of the segment index as it exists in the stream.
|
||||
*/
|
||||
public final int sizeBytes;
|
||||
|
||||
/**
|
||||
* The number of segments.
|
||||
*/
|
||||
public final int length;
|
||||
|
||||
/**
|
||||
* The segment sizes, in bytes.
|
||||
*/
|
||||
public final int[] sizes;
|
||||
|
||||
/**
|
||||
* The segment byte offsets.
|
||||
*/
|
||||
public final long[] offsets;
|
||||
|
||||
/**
|
||||
* The segment durations, in microseconds.
|
||||
*/
|
||||
public final long[] durationsUs;
|
||||
|
||||
/**
|
||||
* The start time of each segment, in microseconds.
|
||||
*/
|
||||
public final long[] timesUs;
|
||||
|
||||
/**
|
||||
* @param sizeBytes The size in bytes of the segment index as it exists in the stream.
|
||||
* @param sizes The segment sizes, in bytes.
|
||||
* @param offsets The segment byte offsets.
|
||||
* @param durationsUs The segment durations, in microseconds.
|
||||
* @param timesUs The start time of each segment, in microseconds.
|
||||
*/
|
||||
public SegmentIndex(int sizeBytes, int[] sizes, long[] offsets, long[] durationsUs,
|
||||
long[] timesUs) {
|
||||
this.sizeBytes = sizeBytes;
|
||||
this.length = sizes.length;
|
||||
this.sizes = sizes;
|
||||
this.offsets = offsets;
|
||||
this.durationsUs = durationsUs;
|
||||
this.timesUs = timesUs;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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.parser.mp4;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/* package */ abstract class Atom {
|
||||
|
||||
public static final int TYPE_avc1 = 0x61766331;
|
||||
public static final int TYPE_esds = 0x65736473;
|
||||
public static final int TYPE_mdat = 0x6D646174;
|
||||
public static final int TYPE_mfhd = 0x6D666864;
|
||||
public static final int TYPE_mp4a = 0x6D703461;
|
||||
public static final int TYPE_tfdt = 0x74666474;
|
||||
public static final int TYPE_tfhd = 0x74666864;
|
||||
public static final int TYPE_trex = 0x74726578;
|
||||
public static final int TYPE_trun = 0x7472756E;
|
||||
public static final int TYPE_sidx = 0x73696478;
|
||||
public static final int TYPE_moov = 0x6D6F6F76;
|
||||
public static final int TYPE_trak = 0x7472616B;
|
||||
public static final int TYPE_mdia = 0x6D646961;
|
||||
public static final int TYPE_minf = 0x6D696E66;
|
||||
public static final int TYPE_stbl = 0x7374626C;
|
||||
public static final int TYPE_avcC = 0x61766343;
|
||||
public static final int TYPE_moof = 0x6D6F6F66;
|
||||
public static final int TYPE_traf = 0x74726166;
|
||||
public static final int TYPE_mvex = 0x6D766578;
|
||||
public static final int TYPE_tkhd = 0x746B6864;
|
||||
public static final int TYPE_mdhd = 0x6D646864;
|
||||
public static final int TYPE_hdlr = 0x68646C72;
|
||||
public static final int TYPE_stsd = 0x73747364;
|
||||
public static final int TYPE_pssh = 0x70737368;
|
||||
public static final int TYPE_sinf = 0x73696E66;
|
||||
public static final int TYPE_schm = 0x7363686D;
|
||||
public static final int TYPE_schi = 0x73636869;
|
||||
public static final int TYPE_tenc = 0x74656E63;
|
||||
public static final int TYPE_encv = 0x656E6376;
|
||||
public static final int TYPE_enca = 0x656E6361;
|
||||
public static final int TYPE_frma = 0x66726D61;
|
||||
public static final int TYPE_saiz = 0x7361697A;
|
||||
public static final int TYPE_uuid = 0x75756964;
|
||||
|
||||
public final int type;
|
||||
|
||||
Atom(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public final static class LeafAtom extends Atom {
|
||||
|
||||
private final ParsableByteArray data;
|
||||
|
||||
public LeafAtom(int type, ParsableByteArray data) {
|
||||
super(type);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public ParsableByteArray getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public final static class ContainerAtom extends Atom {
|
||||
|
||||
public final ArrayList<Atom> children;
|
||||
|
||||
public ContainerAtom(int type) {
|
||||
super(type);
|
||||
children = new ArrayList<Atom>();
|
||||
}
|
||||
|
||||
public void add(Atom atom) {
|
||||
children.add(atom);
|
||||
}
|
||||
|
||||
public LeafAtom getLeafAtomOfType(int type) {
|
||||
for (int i = 0; i < children.size(); i++) {
|
||||
Atom atom = children.get(i);
|
||||
if (atom.type == type) {
|
||||
return (LeafAtom) atom;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ContainerAtom getContainerAtomOfType(int type) {
|
||||
for (int i = 0; i < children.size(); i++) {
|
||||
Atom atom = children.get(i);
|
||||
if (atom.type == type) {
|
||||
return (ContainerAtom) atom;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Atom> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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.parser.mp4;
|
||||
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.MediaCodecInfo.CodecProfileLevel;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides static utility methods for manipulating various types of codec specific data.
|
||||
*/
|
||||
public class CodecSpecificDataUtil {
|
||||
|
||||
private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
|
||||
|
||||
private static final int[] AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE = new int[] {
|
||||
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350
|
||||
};
|
||||
|
||||
private static final int SPS_NAL_UNIT_TYPE = 7;
|
||||
|
||||
private CodecSpecificDataUtil() {}
|
||||
|
||||
/**
|
||||
* Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
|
||||
*
|
||||
* @param audioSpecificConfig
|
||||
* @return A pair consisting of the sample rate in Hz and the channel count.
|
||||
*/
|
||||
public static Pair<Integer, Integer> parseAudioSpecificConfig(byte[] audioSpecificConfig) {
|
||||
int audioObjectType = (audioSpecificConfig[0] >> 3) & 0x1F;
|
||||
int byteOffset = audioObjectType == 5 || audioObjectType == 29 ? 1 : 0;
|
||||
int frequencyIndex = (audioSpecificConfig[byteOffset] & 0x7) << 1
|
||||
| ((audioSpecificConfig[byteOffset + 1] >> 7) & 0x1);
|
||||
Assertions.checkState(frequencyIndex < 13);
|
||||
int sampleRate = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex];
|
||||
int channelCount = (audioSpecificConfig[byteOffset + 1] >> 3) & 0xF;
|
||||
return Pair.create(sampleRate, channelCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
|
||||
*
|
||||
* @param sampleRate The sample rate in Hz.
|
||||
* @param numChannels The number of channels
|
||||
* @return The AudioSpecificConfig.
|
||||
*/
|
||||
public static byte[] buildAudioSpecificConfig(int sampleRate, int numChannels) {
|
||||
int sampleRateIndex = -1;
|
||||
for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE.length; ++i) {
|
||||
if (sampleRate == AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[i]) {
|
||||
sampleRateIndex = i;
|
||||
}
|
||||
}
|
||||
// The full specification for AudioSpecificConfig is stated in ISO 14496-3 Section 1.6.2.1
|
||||
byte[] csd = new byte[2];
|
||||
csd[0] = (byte) ((2 /* AAC LC */ << 3) | (sampleRateIndex >> 1));
|
||||
csd[1] = (byte) (((sampleRateIndex & 0x1) << 7) | (numChannels << 3));
|
||||
return csd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a NAL unit consisting of the NAL start code followed by the specified data.
|
||||
*
|
||||
* @param data An array containing the data that should follow the NAL start code.
|
||||
* @param offset The start offset into {@code data}.
|
||||
* @param length The number of bytes to copy from {@code data}
|
||||
* @return The constructed NAL unit.
|
||||
*/
|
||||
public static byte[] buildNalUnit(byte[] data, int offset, int length) {
|
||||
byte[] nalUnit = new byte[length + NAL_START_CODE.length];
|
||||
System.arraycopy(NAL_START_CODE, 0, nalUnit, 0, NAL_START_CODE.length);
|
||||
System.arraycopy(data, offset, nalUnit, NAL_START_CODE.length, length);
|
||||
return nalUnit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an array of NAL units.
|
||||
* <p>
|
||||
* If the input consists of NAL start code delimited units, then the returned array consists of
|
||||
* the split NAL units, each of which is still prefixed with the NAL start code. For any other
|
||||
* input, null is returned.
|
||||
*
|
||||
* @param data An array of data.
|
||||
* @return The individual NAL units, or null if the input did not consist of NAL start code
|
||||
* delimited units.
|
||||
*/
|
||||
public static byte[][] splitNalUnits(byte[] data) {
|
||||
if (!isNalStartCode(data, 0)) {
|
||||
// data does not consist of NAL start code delimited units.
|
||||
return null;
|
||||
}
|
||||
List<Integer> starts = new ArrayList<Integer>();
|
||||
int nalUnitIndex = 0;
|
||||
do {
|
||||
starts.add(nalUnitIndex);
|
||||
nalUnitIndex = findNalStartCode(data, nalUnitIndex + NAL_START_CODE.length);
|
||||
} while (nalUnitIndex != -1);
|
||||
byte[][] split = new byte[starts.size()][];
|
||||
for (int i = 0; i < starts.size(); i++) {
|
||||
int startIndex = starts.get(i);
|
||||
int endIndex = i < starts.size() - 1 ? starts.get(i + 1) : data.length;
|
||||
byte[] nal = new byte[endIndex - startIndex];
|
||||
System.arraycopy(data, startIndex, nal, 0, nal.length);
|
||||
split[i] = nal;
|
||||
}
|
||||
return split;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the next occurrence of the NAL start code from a given index.
|
||||
*
|
||||
* @param data The data in which to search.
|
||||
* @param index The first index to test.
|
||||
* @return The index of the first byte of the found start code, or -1.
|
||||
*/
|
||||
private static int findNalStartCode(byte[] data, int index) {
|
||||
int endIndex = data.length - NAL_START_CODE.length;
|
||||
for (int i = index; i <= endIndex; i++) {
|
||||
if (isNalStartCode(data, i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether there exists a NAL start code at a given index.
|
||||
*
|
||||
* @param data The data.
|
||||
* @param index The index to test.
|
||||
* @return Whether there exists a start code that begins at {@code index}.
|
||||
*/
|
||||
private static boolean isNalStartCode(byte[] data, int index) {
|
||||
if (data.length - index <= NAL_START_CODE.length) {
|
||||
return false;
|
||||
}
|
||||
for (int j = 0; j < NAL_START_CODE.length; j++) {
|
||||
if (data[index + j] != NAL_START_CODE[j]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an SPS NAL unit.
|
||||
*
|
||||
* @param spsNalUnit The NAL unit.
|
||||
* @return A pair consisting of AVC profile and level constants, as defined in
|
||||
* {@link CodecProfileLevel}. Null if the input data was not an SPS NAL unit.
|
||||
*/
|
||||
public static Pair<Integer, Integer> parseSpsNalUnit(byte[] spsNalUnit) {
|
||||
// SPS NAL unit:
|
||||
// - Start prefix (4 bytes)
|
||||
// - Forbidden zero bit (1 bit)
|
||||
// - NAL ref idx (2 bits)
|
||||
// - NAL unit type (5 bits)
|
||||
// - Profile idc (8 bits)
|
||||
// - Constraint bits (3 bits)
|
||||
// - Reserved bits (5 bits)
|
||||
// - Level idx (8 bits)
|
||||
if (isNalStartCode(spsNalUnit, 0) && spsNalUnit.length == 8
|
||||
&& (spsNalUnit[5] & 0x1F) == SPS_NAL_UNIT_TYPE) {
|
||||
return Pair.create(parseAvcProfile(spsNalUnit), parseAvcLevel(spsNalUnit));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private static int parseAvcProfile(byte[] data) {
|
||||
int profileIdc = data[6] & 0xFF;
|
||||
switch (profileIdc) {
|
||||
case 0x42:
|
||||
return CodecProfileLevel.AVCProfileBaseline;
|
||||
case 0x4d:
|
||||
return CodecProfileLevel.AVCProfileMain;
|
||||
case 0x58:
|
||||
return CodecProfileLevel.AVCProfileExtended;
|
||||
case 0x64:
|
||||
return CodecProfileLevel.AVCProfileHigh;
|
||||
case 0x6e:
|
||||
return CodecProfileLevel.AVCProfileHigh10;
|
||||
case 0x7a:
|
||||
return CodecProfileLevel.AVCProfileHigh422;
|
||||
case 0xf4:
|
||||
return CodecProfileLevel.AVCProfileHigh444;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private static int parseAvcLevel(byte[] data) {
|
||||
int levelIdc = data[8] & 0xFF;
|
||||
switch (levelIdc) {
|
||||
case 9:
|
||||
return CodecProfileLevel.AVCLevel1b;
|
||||
case 10:
|
||||
return CodecProfileLevel.AVCLevel1;
|
||||
case 11:
|
||||
return CodecProfileLevel.AVCLevel11;
|
||||
case 12:
|
||||
return CodecProfileLevel.AVCLevel12;
|
||||
case 13:
|
||||
return CodecProfileLevel.AVCLevel13;
|
||||
case 20:
|
||||
return CodecProfileLevel.AVCLevel2;
|
||||
case 21:
|
||||
return CodecProfileLevel.AVCLevel21;
|
||||
case 22:
|
||||
return CodecProfileLevel.AVCLevel22;
|
||||
case 30:
|
||||
return CodecProfileLevel.AVCLevel3;
|
||||
case 31:
|
||||
return CodecProfileLevel.AVCLevel31;
|
||||
case 32:
|
||||
return CodecProfileLevel.AVCLevel32;
|
||||
case 40:
|
||||
return CodecProfileLevel.AVCLevel4;
|
||||
case 41:
|
||||
return CodecProfileLevel.AVCLevel41;
|
||||
case 42:
|
||||
return CodecProfileLevel.AVCLevel42;
|
||||
case 50:
|
||||
return CodecProfileLevel.AVCLevel5;
|
||||
case 51:
|
||||
return CodecProfileLevel.AVCLevel51;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user