Merge pull request #11 from google/dev-v2

Merge from dev-v2
This commit is contained in:
ybai001 2020-02-06 09:44:18 +08:00 committed by GitHub
commit 29f9cd3415
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1988 changed files with 225646 additions and 49940 deletions

57
.github/ISSUE_TEMPLATE/bug.md vendored Normal file
View File

@ -0,0 +1,57 @@
---
name: Bug report
about: Issue template for a bug report.
title: ''
labels: bug, needs triage
assignees: ''
---
Before filing a bug:
-----------------------
- Search existing issues, including issues that are closed:
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- Consult our developer website, which can be found at https://exoplayer.dev/.
It provides detailed information about supported formats and devices.
- Learn how to create useful log output by using the EventLogger:
https://exoplayer.dev/listening-to-player-events.html#using-eventlogger
- Rule out issues in your own code. A good way to do this is to try and
reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer
demo app can be found here:
http://exoplayer.dev/demo-application.html.
When reporting a bug:
-----------------------
Fill out the sections below, leaving the headers but replacing the content. If
you're unable to provide certain information, please explain why in the relevant
section. We may close issues if they do not include sufficient information.
### [REQUIRED] Issue description
Describe the issue in detail, including observed and expected behavior.
### [REQUIRED] Reproduction steps
Describe how the issue can be reproduced, ideally using the ExoPlayer demo app
or a small sample app that youre able to share as source code on GitHub.
### [REQUIRED] Link to test content
Provide a JSON snippet for the demo apps media.exolist.json file, or a link to
media that reproduces the issue. If you don't wish to post it publicly, please
submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
in the format "Issue #1234", where "#1234" should be replaced with your issue
number. Provide all the metadata we'd need to play the content like drm license
urls or similar. If the content is accessible only in certain countries or
regions, please say so.
### [REQUIRED] A full bug report captured from the device
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
log snippet is NOT sufficient. Please attach the captured bug report as a file.
If you don't wish to post it publicly, please submit the issue, then email the
bug report to dev.exoplayer@gmail.com using a subject in the format
"Issue #1234", where "#1234" should be replaced with your issue number.
### [REQUIRED] Version of ExoPlayer being used
Specify the absolute version number. Avoid using terms such as "latest".
### [REQUIRED] Device(s) and version(s) of Android being used
Specify the devices and versions of Android on which the issue can be
reproduced, and how easily it reproduces. If possible, please test on multiple
devices and Android versions.

View File

@ -0,0 +1,30 @@
---
name: Feature request
about: Issue template for a feature request.
title: ''
labels: enhancement, needs triage
assignees: ''
---
Before filing a feature request:
-----------------------
- Search existing open issues, specifically with the label enhancement:
https://github.com/google/ExoPlayer/labels/enhancement
- Search existing pull requests: https://github.com/google/ExoPlayer/pulls
When filing a feature request:
-----------------------
Fill out the sections below, leaving the headers but replacing the content. If
you're unable to provide certain information, please explain why in the relevant
section. We may close issues if they do not include sufficient information.
### [REQUIRED] Use case description
Describe the use case or problem you are trying to solve in detail. If there are
any standards or specifications involved, please provide the relevant details.
### Proposed solution
A clear and concise description of your proposed solution, if you have one.
### Alternatives considered
A clear and concise description of any alternative solutions you considered,
if applicable.

50
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@ -0,0 +1,50 @@
---
name: Question
about: Issue template for a question.
title: ''
labels: question, needs triage
assignees: ''
---
Before filing a question:
-----------------------
- This issue tracker is intended ExoPlayer specific questions. If you're asking
a general Android development question, please do so on Stack Overflow.
- Search existing issues, including issues that are closed. Its often the
quickest way to get an answer!
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- Consult our developer website, which can be found at https://exoplayer.dev/.
It provides detailed information about supported formats, devices as well as
information about how to use the ExoPlayer library.
- The ExoPlayer library Javadoc can be found at
https://exoplayer.dev/doc/reference/
When filing a question:
-----------------------
Fill out the sections below, leaving the headers but replacing the content. If
you're unable to provide certain information, please explain why in the relevant
section. We may close issues if they do not include sufficient information.
### [REQUIRED] Searched documentation and issues
Tell us where youve already looked for an answer to your question. Its
important for us to know this so that we can improve our documentation.
### [REQUIRED] Question
Describe your question in detail.
### A full bug report captured from the device
In case your question refers to a problem you are seeing in your app, capture a
full bug report using "adb bugreport". Please attach the captured bug report as
a file. If you don't wish to post it publicly, please submit the issue, then
email the bug report to dev.exoplayer@gmail.com using a subject in the format
"Issue #1234", where "#1234" should be replaced with your issue number.
### Link to test content
In case your question is related to a piece of media, which you are trying to
play, please provide a JSON snippet for the demo apps media.exolist.json file,
or a link to media that reproduces the issue. If you don't wish to post it
publicly, please submit the issue, then email the link to
dev.exoplayer@gmail.com using a subject in the format "Issue #1234", where
"#1234" should be replaced with your issue number. Provide all the metadata we'd
need to play the content like drm license urls or similar. If the content is
accessible only in certain countries or regions, please say so.

14
.gitignore vendored
View File

@ -37,16 +37,30 @@ local.properties
proguard.cfg proguard.cfg
proguard-project.txt proguard-project.txt
# Bazel
bazel-bin
bazel-genfiles
bazel-out
bazel-testlogs
# Other # Other
.DS_Store .DS_Store
cmake-build-debug
dist dist
tmp tmp
# External native builds
.externalNativeBuild
# VP9 extension # VP9 extension
extensions/vp9/src/main/jni/libvpx extensions/vp9/src/main/jni/libvpx
extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libvpx_android_configs
extensions/vp9/src/main/jni/libyuv extensions/vp9/src/main/jni/libyuv
# AV1 extension
extensions/av1/src/main/jni/cpu_features
extensions/av1/src/main/jni/libgav1
# Opus extension # Opus extension
extensions/opus/src/main/jni/libopus extensions/opus/src/main/jni/libopus

81
.hgignore Normal file
View File

@ -0,0 +1,81 @@
# Mercurial's .hgignore files can only be used in the root directory.
# You can still apply these rules by adding
# include:path/to/this/directory/.hgignore to the top-level .hgignore file.
# Ensure same syntax as in .gitignore can be used
syntax:glob
# Android generated
bin
gen
libs
obj
lint.xml
# IntelliJ IDEA & Android Studio
.idea
*.iml
*.ipr
*.iws
classes
gen-external-apklibs
*.li
# Eclipse
.project
.classpath
.settings
.checkstyle
.cproject
# Gradle
.gradle
build
buildout
out
# Maven
target
release.properties
pom.xml.*
# Ant
ant.properties
local.properties
proguard.cfg
proguard-project.txt
# Bazel
bazel-bin
bazel-genfiles
bazel-out
bazel-testlogs
# Other
.DS_Store
cmake-build-debug
dist
tmp
# VP9 extension
extensions/vp9/src/main/jni/libvpx
extensions/vp9/src/main/jni/libvpx_android_configs
extensions/vp9/src/main/jni/libyuv
# AV1 extension
extensions/av1/src/main/jni/libgav1
# Opus extension
extensions/opus/src/main/jni/libopus
# FLAC extension
extensions/flac/src/main/jni/flac
# FFmpeg extension
extensions/ffmpeg/src/main/jni/ffmpeg
# Cronet extension
extensions/cronet/jniLibs/*
!extensions/cronet/jniLibs/README.md
extensions/cronet/libs/*
!extensions/cronet/libs/README.md

495
.idea/codeStyleSettings.xml generated Normal file
View File

@ -0,0 +1,495 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleSettingsManager">
<option name="PER_PROJECT_SETTINGS">
<value>
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="false" />
<option name="SMART_TABS" value="false" />
<option name="LABEL_INDENT_SIZE" value="0" />
<option name="LABEL_INDENT_ABSOLUTE" value="false" />
<option name="USE_RELATIVE_INDENTS" value="false" />
</value>
</option>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
</value>
</option>
<option name="RIGHT_MARGIN" value="100" />
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
<option name="JD_P_AT_EMPTY_LINES" value="false" />
<option name="JD_KEEP_EMPTY_PARAMETER" value="false" />
<option name="JD_KEEP_EMPTY_EXCEPTION" value="false" />
<option name="JD_KEEP_EMPTY_RETURN" value="false" />
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="WRAP_COMMENTS" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
<option name="LAYOUT_SETTINGS">
<value>
<option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" />
</value>
</option>
</AndroidXmlCodeStyleSettings>
<Objective-C>
<option name="INDENT_NAMESPACE_MEMBERS" value="0" />
<option name="INDENT_C_STRUCT_MEMBERS" value="2" />
<option name="INDENT_CLASS_MEMBERS" value="2" />
<option name="INDENT_VISIBILITY_KEYWORDS" value="1" />
<option name="INDENT_INSIDE_CODE_BLOCK" value="2" />
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
<option name="FUNCTION_PARAMETERS_WRAP" value="5" />
<option name="FUNCTION_CALL_ARGUMENTS_WRAP" value="5" />
<option name="TEMPLATE_CALL_ARGUMENTS_WRAP" value="5" />
<option name="TEMPLATE_CALL_ARGUMENTS_ALIGN_MULTILINE" value="true" />
<option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" />
<option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" />
</Objective-C>
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cc" header="h" />
<pair source="c" header="h" />
</extensions>
</Objective-C-extensions>
<XML>
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_RESOURCES" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<option name="PARENT_SETTINGS_INSTALLED" value="true" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ObjectiveC">
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
<option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
<option name="BLANK_LINES_AFTER_IMPORTS" value="0" />
<option name="BLANK_LINES_AROUND_CLASS" value="0" />
<option name="BLANK_LINES_AROUND_METHOD" value="0" />
<option name="BLANK_LINES_AROUND_METHOD_IN_INTERFACE" value="0" />
<option name="ALIGN_MULTILINE_BINARY_OPERATION" value="false" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:.*Style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_weight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_margin</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginTop</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginBottom</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginStart</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginEnd</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginLeft</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginRight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:padding</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingTop</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingBottom</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingStart</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingEnd</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingLeft</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingRight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/tools</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</value>
</option>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</component>
</project>

View File

@ -16,9 +16,8 @@ all of the information requested in the issue template.
## Pull requests ## ## Pull requests ##
We will also consider high quality pull requests. These should normally merge We will also consider high quality pull requests. These should normally merge
into the `dev-vX` branch with the highest major version number. Bug fixes may into the `dev-v2` branch. Before a pull request can be accepted you must submit
be suitable for merging into older `dev-vX` branches. Before a pull request can a Contributor License Agreement, as described below.
be accepted you must submit a Contributor License Agreement, as described below.
[dev]: https://github.com/google/ExoPlayer/tree/dev [dev]: https://github.com/google/ExoPlayer/tree/dev

View File

@ -1,44 +0,0 @@
*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION ***
Before filing an issue:
-----------------------
- Search existing issues, including issues that are closed.
- Consult our FAQs, supported devices and supported formats pages. These can be
found at https://google.github.io/ExoPlayer/.
- Rule out issues in your own code. A good way to do this is to try and
reproduce the issue in the ExoPlayer demo app.
- This issue tracker is intended for bugs, feature requests and ExoPlayer
specific questions. If you're asking a general Android development question,
please do so on Stack Overflow.
When reporting a bug:
-----------------------
Fill out the sections below, leaving the headers but replacing the content. If
you're unable to provide certain information, please explain why in the relevant
section. We may close issues if they do not include sufficient information.
### Issue description
Describe the issue in detail, including observed and expected behavior.
### Reproduction steps
Describe how the issue can be reproduced, ideally using the ExoPlayer demo app.
### Link to test content
Provide a link to media that reproduces the issue. If you don't wish to post it
publicly, please submit the issue, then email the link to
dev.exoplayer@gmail.com including the issue number in the subject line.
### Version of ExoPlayer being used
Specify the absolute version number. Avoid using terms such as "latest".
### Device(s) and version(s) of Android being used
Specify the devices and versions of Android on which the issue can be
reproduced, and how easily it reproduces. If possible, please test on multiple
devices and Android versions.
### A full bug report captured from the device
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
log snippet is NOT sufficient. Please attach the captured bug report as a file.
If you don't wish to post it publicly, please submit the issue, then email the
bug report to dev.exoplayer@gmail.com including the issue number in the subject
line.

123
README.md
View File

@ -9,47 +9,61 @@ and extend, and can be updated through Play Store application updates.
## Documentation ## ## Documentation ##
* The [developer guide][] provides a wealth of information to help you get * The [developer guide][] provides a wealth of information.
started. * The [class reference][] documents ExoPlayer classes.
* The [class reference][] documents the ExoPlayer library classes.
* The [release notes][] document the major changes in each release. * The [release notes][] document the major changes in each release.
* Follow our [developer blog][] to keep up to date with the latest ExoPlayer
developments!
[developer guide]: https://google.github.io/ExoPlayer/guide.html [developer guide]: https://exoplayer.dev/guide.html
[class reference]: https://google.github.io/ExoPlayer/doc/reference [class reference]: https://exoplayer.dev/doc/reference
[release notes]: https://github.com/google/ExoPlayer/blob/dev-v2/RELEASENOTES.md [release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
[developer blog]: https://medium.com/google-exoplayer
## Using ExoPlayer ## ## Using ExoPlayer ##
ExoPlayer modules can be obtained from JCenter. It's also possible to clone the
repository and depend on the modules locally.
### From JCenter ###
#### 1. Add repositories ####
The easiest way to get started using ExoPlayer is to add it as a gradle The easiest way to get started using ExoPlayer is to add it as a gradle
dependency. You need to make sure you have the jcenter repository included in dependency. You need to make sure you have the Google and JCenter repositories
the `build.gradle` file in the root of your project: included in the `build.gradle` file in the root of your project:
```gradle ```gradle
repositories { repositories {
google()
jcenter() jcenter()
} }
``` ```
Next add a gradle compile dependency to the `build.gradle` file of your app #### 2. Add ExoPlayer module dependencies ####
module. The following will add a dependency to the full ExoPlayer library:
Next add a dependency in the `build.gradle` file of your app module. The
following will add a dependency to the full library:
```gradle ```gradle
compile 'com.google.android.exoplayer:exoplayer:r2.X.X' implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
``` ```
where `r2.X.X` is your preferred version. Alternatively, you can depend on only where `2.X.X` is your preferred version.
the library modules that you actually need. For example the following will add
dependencies on the Core, DASH and UI library modules, as might be required for As an alternative to the full library, you can depend on only the library
an app that plays DASH content: modules that you actually need. For example the following will add dependencies
on the Core, DASH and UI library modules, as might be required for an app that
plays DASH content:
```gradle ```gradle
compile 'com.google.android.exoplayer:exoplayer-core:r2.X.X' implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
compile 'com.google.android.exoplayer:exoplayer-dash:r2.X.X' implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'
compile 'com.google.android.exoplayer:exoplayer-ui:r2.X.X' implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'
``` ```
The available modules are listed below. Adding a dependency to the full The available library modules are listed below. Adding a dependency to the full
ExoPlayer library is equivalent to adding dependencies on all of the modules library is equivalent to adding dependencies on all of the library modules
individually. individually.
* `exoplayer-core`: Core functionality (required). * `exoplayer-core`: Core functionality (required).
@ -58,25 +72,70 @@ individually.
* `exoplayer-smoothstreaming`: Support for SmoothStreaming content. * `exoplayer-smoothstreaming`: Support for SmoothStreaming content.
* `exoplayer-ui`: UI components and resources for use with ExoPlayer. * `exoplayer-ui`: UI components and resources for use with ExoPlayer.
For more details, see the project on [Bintray][]. For information about the In addition to library modules, ExoPlayer has multiple extension modules that
latest versions, see the [Release notes][]. depend on external libraries to provide additional functionality. Some
extensions are available from JCenter, whereas others must be built manually.
Browse the [extensions directory][] and their individual READMEs for details.
More information on the library and extension modules that are available from
JCenter can be found on [Bintray][].
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
[Bintray]: https://bintray.com/google/exoplayer [Bintray]: https://bintray.com/google/exoplayer
[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
#### 3. Turn on Java 8 support ####
If not enabled already, you also need to turn on Java 8 support in all
`build.gradle` files depending on ExoPlayer, by adding the following to the
`android` section:
```gradle
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
```
### Locally ###
Cloning the repository and depending on the modules locally is required when
using some ExoPlayer extension modules. It's also a suitable approach if you
want to make local changes to ExoPlayer, or if you want to use a development
branch.
First, clone the repository into a local directory and checkout the desired
branch:
```sh
git clone https://github.com/google/ExoPlayer.git
cd ExoPlayer
git checkout release-v2
```
Next, add the following to your project's `settings.gradle` file, replacing
`path/to/exoplayer` with the path to your local copy:
```gradle
gradle.ext.exoplayerRoot = 'path/to/exoplayer'
gradle.ext.exoplayerModulePrefix = 'exoplayer-'
apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle')
```
You should now see the ExoPlayer modules appear as part of your project. You can
depend on them as you would on any other local module, for example:
```gradle
implementation project(':exoplayer-library-core')
implementation project(':exoplayer-library-dash')
implementation project(':exoplayer-library-ui')
```
## Developing ExoPlayer ## ## Developing ExoPlayer ##
#### Project branches #### #### Project branches ####
* The project has `dev-vX` and `release-vX` branches, where `X` is the major * Development work happens on the `dev-v2` branch. Pull requests should
version number. normally be made to this branch.
* Most development work happens on the `dev-vX` branch with the highest major * The `release-v2` branch holds the most recent release.
version number. Pull requests should normally be made to this branch.
* Bug fixes may be submitted to older `dev-vX` branches. When doing this, the
same (or an equivalent) fix should also be submitted to all subsequent
`dev-vX` branches.
* A `release-vX` branch holds the most recent stable release for major version
`X`.
#### Using Android Studio #### #### Using Android Studio ####

File diff suppressed because it is too large Load Diff

View File

@ -13,43 +13,22 @@
// limitations under the License. // limitations under the License.
buildscript { buildscript {
repositories { repositories {
google()
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.3.1' classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'com.novoda:bintray-release:0.4.0' classpath 'com.novoda:bintray-release:0.9.1'
} classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0'
// Workaround for the following test coverage issue. Remove when fixed:
// https://code.google.com/p/android/issues/detail?id=226070
configurations.all {
resolutionStrategy {
force 'org.jacoco:org.jacoco.report:0.7.4.201502262128'
force 'org.jacoco:org.jacoco.core:0.7.4.201502262128'
}
} }
} }
allprojects { allprojects {
repositories { repositories {
google()
jcenter() jcenter()
} }
project.ext { project.ext {
// Important: ExoPlayer specifies a minSdkVersion of 9 because various exoplayerPublishEnabled = false
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality
// provided by the library requires API level 16 or greater.
minSdkVersion = 9
compileSdkVersion = 25
targetSdkVersion = 25
buildToolsVersion = '25'
testSupportLibraryVersion = '0.5'
supportLibraryVersion = '25.3.1'
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
releaseRepoName = getBintrayRepo()
releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.4.4'
releaseWebsite = 'https://github.com/google/ExoPlayer'
} }
if (it.hasProperty('externalBuildDir')) { if (it.hasProperty('externalBuildDir')) {
if (!new File(externalBuildDir).isAbsolute()) { if (!new File(externalBuildDir).isAbsolute()) {
@ -57,12 +36,7 @@ allprojects {
} }
buildDir = "${externalBuildDir}/${project.name}" buildDir = "${externalBuildDir}/${project.name}"
} }
} group = 'com.google.android.exoplayer'
def getBintrayRepo() {
boolean publicRepo = hasProperty('publicRepo') &&
property('publicRepo').toBoolean()
return publicRepo ? 'exoplayer' : 'exoplayer-test'
} }
apply from: 'javadoc_combined.gradle' apply from: 'javadoc_combined.gradle'

43
constants.gradle Normal file
View File

@ -0,0 +1,43 @@
// Copyright (C) 2017 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.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.11.2'
releaseVersionCode = 2011002
minSdkVersion = 16
appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved
compileSdkVersion = 29
dexmakerVersion = '2.21.0'
junitVersion = '4.13-rc-2'
guavaVersion = '23.5-android'
mockitoVersion = '2.25.0'
robolectricVersion = '4.3.1'
checkerframeworkVersion = '2.5.0'
jsr305Version = '3.0.2'
kotlinAnnotationsVersion = '1.3.31'
androidxAnnotationVersion = '1.1.0'
androidxAppCompatVersion = '1.1.0'
androidxCollectionVersion = '1.1.0'
androidxMediaVersion = '1.0.1'
androidxTestCoreVersion = '1.2.0'
androidxTestJUnitVersion = '1.1.1'
androidxTestRunnerVersion = '1.2.0'
androidxTestRulesVersion = '1.2.0'
truthVersion = '1.0'
modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix
}
}

68
core_settings.gradle Normal file
View File

@ -0,0 +1,68 @@
// Copyright (C) 2017 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.
def rootDir = gradle.ext.exoplayerRoot
def modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix
}
include modulePrefix + 'library'
include modulePrefix + 'library-common'
include modulePrefix + 'library-core'
include modulePrefix + 'library-dash'
include modulePrefix + 'library-extractor'
include modulePrefix + 'library-hls'
include modulePrefix + 'library-smoothstreaming'
include modulePrefix + 'library-ui'
include modulePrefix + 'testutils'
include modulePrefix + 'extension-av1'
include modulePrefix + 'extension-ffmpeg'
include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr'
include modulePrefix + 'extension-ima'
include modulePrefix + 'extension-cast'
include modulePrefix + 'extension-cronet'
include modulePrefix + 'extension-mediasession'
include modulePrefix + 'extension-okhttp'
include modulePrefix + 'extension-opus'
include modulePrefix + 'extension-vp9'
include modulePrefix + 'extension-rtmp'
include modulePrefix + 'extension-leanback'
include modulePrefix + 'extension-jobdispatcher'
include modulePrefix + 'extension-workmanager'
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
project(modulePrefix + 'library-common').projectDir = new File(rootDir, 'library/common')
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash')
project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor')
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1')
project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager')

View File

@ -1,456 +0,0 @@
[
{
"name": "YouTube DASH",
"samples": [
{
"name": "Google Glass (MP4,H264)",
"uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
"extension": "mpd"
},
{
"name": "Google Play (MP4,H264)",
"uri": "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0",
"extension": "mpd"
},
{
"name": "Google Glass (WebM,VP9)",
"uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0",
"extension": "mpd"
},
{
"name": "Google Play (WebM,VP9)",
"uri": "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0",
"extension": "mpd"
}
]
},
{
"name": "Widevine DASH Policy Tests (GTS)",
"samples": [
{
"name": "WV: HDCP not specified",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1c&provider=widevine_test"
},
{
"name": "WV: HDCP not required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=48fcc369939ac96c&provider=widevine_test"
},
{
"name": "WV: HDCP required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=e06c39f1151da3df&provider=widevine_test"
},
{
"name": "WV: Secure video path required (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
},
{
"name": "WV: Secure video path required (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
},
{
"name": "WV: Secure video path required (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
},
{
"name": "WV: HDCP + secure video path required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=efd045b1eb61888a&provider=widevine_test"
},
{
"name": "WV: 30s license duration (fails at ~30s)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=f9a34cab7b05881a&provider=widevine_test"
}
]
},
{
"name": "Widevine HDCP Capabilities Tests",
"samples": [
{
"name": "WV: HDCP: None (not required)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_None&provider=widevine_test"
},
{
"name": "WV: HDCP: 1.0 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V1&provider=widevine_test"
},
{
"name": "WV: HDCP: 2.0 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2&provider=widevine_test"
},
{
"name": "WV: HDCP: 2.1 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_1&provider=widevine_test"
},
{
"name": "WV: HDCP: 2.2 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_2&provider=widevine_test"
},
{
"name": "WV: HDCP: No digital output",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_NO_DIGTAL_OUTPUT&provider=widevine_test"
}
]
},
{
"name": "Widevine DASH: MP4,H264",
"samples": [
{
"name": "WV: Clear SD & HD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
},
{
"name": "WV: Clear SD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd"
},
{
"name": "WV: Clear HD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd"
},
{
"name": "WV: Clear UHD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
},
{
"name": "WV: Secure SD & HD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}
]
},
{
"name": "Widevine DASH: WebM,VP9",
"samples": [
{
"name": "WV: Clear SD & HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd"
},
{
"name": "WV: Clear SD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd"
},
{
"name": "WV: Clear HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd"
},
{
"name": "WV: Clear UHD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd"
},
{
"name": "WV: Secure Fullsample SD & HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Fullsample SD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Fullsample HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Fullsample UHD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample SD & HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample SD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample UHD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}
]
},
{
"name": "Widevine DASH: MP4,H265",
"samples": [
{
"name": "WV: Clear SD & HD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd"
},
{
"name": "WV: Clear SD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd"
},
{
"name": "WV: Clear HD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd"
},
{
"name": "WV: Clear UHD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd"
},
{
"name": "WV: Secure SD & HD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}
]
},
{
"name": "ClearKey DASH",
"samples": [
{
"name": "Big Buck Bunny (CENC ClearKey)",
"uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd",
"extension": "mpd",
"drm_scheme": "cenc",
"drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json"
}
]
},
{
"name": "SmoothStreaming",
"samples": [
{
"name": "Super speed",
"uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism"
},
{
"name": "Super speed (PlayReady)",
"uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
"drm_scheme": "playready"
}
]
},
{
"name": "HLS",
"samples": [
{
"name": "Apple 4x3 basic stream",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8"
},
{
"name": "Apple 16x9 basic stream",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
},
{
"name": "Apple master playlist advanced (TS)",
"uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_example_v2/master.m3u8"
},
{
"name": "Apple master playlist advanced (fMP4)",
"uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_fmp4_example/master.m3u8"
},
{
"name": "Apple TS media playlist",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8"
},
{
"name": "Apple AAC media playlist",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
},
{
"name": "Apple ID3 metadata",
"uri": "http://devimages.apple.com/samplecode/adDemo/ad.m3u8"
}
]
},
{
"name": "Misc",
"samples": [
{
"name": "Dizzy",
"uri": "http://html5demos.com/assets/dizzy.mp4"
},
{
"name": "Apple AAC 10s",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
},
{
"name": "Apple TS 10s",
"uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
},
{
"name": "Android screens (Matroska)",
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
},
{
"name": "Big Buck Bunny (MP4 Video)",
"uri": "http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube&sparams=ip,ipbits,expire,source,id&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=513F28C7FDCBEC60A66C86C9A393556C99DC47FB.04C88036EEE12565A1ED864A875A58F15D8B5300&key=ik0"
},
{
"name": "Screens 360P (WebM,VP9,No Audio)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
},
{
"name": "Screens 480p (FMP4,H264,No Audio)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4"
},
{
"name": "Screens 1080p (FMP4,H264, No Audio)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4"
},
{
"name": "Screens (FMP4,AAC Audio)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
},
{
"name": "Google Play (MP3 Audio)",
"uri": "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
},
{
"name": "Google Play (Ogg/Vorbis Audio)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
},
{
"name": "Google Glass (WebM Video with Vorbis Audio)",
"uri": "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm"
},
{
"name": "Google Glass (VP9 in MP4/ISO-BMFF)",
"uri": "http://demos.webmproject.org/exoplayer/glass.mp4"
},
{
"name": "Google Glass DASH - VP9 and Opus",
"uri": "http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9_opus.mpd"
},
{
"name": "Big Buck Bunny (FLV Video)",
"uri": "http://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
}
]
},
{
"name": "Playlists",
"samples": [
{
"name": "Cats -> Dogs",
"playlist": [
{
"uri": "http://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
}
]
},
{
"name": "Audio -> Video -> Audio",
"playlist": [
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
},
{
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
},
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
}
]
},
{
"name": "Clear -> Enc -> Clear -> Enc -> Enc",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
"playlist": [
{
"uri": "http://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
},
{
"uri": "http://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
},
{
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
}
]
}
]
}
]

View File

@ -1,52 +0,0 @@
/*
* Copyright (C) 2016 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.exoplayer2.demo;
import android.app.Application;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Util;
/**
* Placeholder application to facilitate overriding Application methods for debugging and testing.
*/
public class DemoApplication extends Application {
protected String userAgent;
@Override
public void onCreate() {
super.onCreate();
userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
}
public DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
return new DefaultDataSourceFactory(this, bandwidthMeter,
buildHttpDataSourceFactory(bandwidthMeter));
}
public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter);
}
public boolean useExtensionRenderers() {
return BuildConfig.FLAVOR.equals("withExtensions");
}
}

View File

@ -1,86 +0,0 @@
/*
* Copyright (C) 2017 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.exoplayer2.demo;
import android.text.TextUtils;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Locale;
/**
* Utility methods for demo application.
*/
/*package*/ final class DemoUtil {
/**
* Builds a track name for display.
*
* @param format {@link Format} of the track.
* @return a generated name specific to the track.
*/
public static String buildTrackName(Format format) {
String trackName;
if (MimeTypes.isVideo(format.sampleMimeType)) {
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(
buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)),
buildSampleMimeTypeString(format));
} else if (MimeTypes.isAudio(format.sampleMimeType)) {
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator(
buildLanguageString(format), buildAudioPropertyString(format)),
buildBitrateString(format)), buildTrackIdString(format)),
buildSampleMimeTypeString(format));
} else {
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format),
buildBitrateString(format)), buildTrackIdString(format)),
buildSampleMimeTypeString(format));
}
return trackName.length() == 0 ? "unknown" : trackName;
}
private static String buildResolutionString(Format format) {
return format.width == Format.NO_VALUE || format.height == Format.NO_VALUE
? "" : format.width + "x" + format.height;
}
private static String buildAudioPropertyString(Format format) {
return format.channelCount == Format.NO_VALUE || format.sampleRate == Format.NO_VALUE
? "" : format.channelCount + "ch, " + format.sampleRate + "Hz";
}
private static String buildLanguageString(Format format) {
return TextUtils.isEmpty(format.language) || "und".equals(format.language) ? ""
: format.language;
}
private static String buildBitrateString(Format format) {
return format.bitrate == Format.NO_VALUE ? ""
: String.format(Locale.US, "%.2fMbit", format.bitrate / 1000000f);
}
private static String joinWithSeparator(String first, String second) {
return first.length() == 0 ? second : (second.length() == 0 ? first : first + ", " + second);
}
private static String buildTrackIdString(Format format) {
return format.id == null ? "" : ("id:" + format.id);
}
private static String buildSampleMimeTypeString(Format format) {
return format.sampleMimeType == null ? "" : format.sampleMimeType;
}
private DemoUtil() {}
}

View File

@ -1,464 +0,0 @@
/*
* Copyright (C) 2016 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.exoplayer2.demo;
import android.os.SystemClock;
import android.util.Log;
import android.view.Surface;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.GeobFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.Locale;
/**
* Logs player events using {@link Log}.
*/
/* package */ final class EventLogger implements ExoPlayer.EventListener,
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener,
MetadataRenderer.Output {
private static final String TAG = "EventLogger";
private static final int MAX_TIMELINE_ITEM_LINES = 3;
private static final NumberFormat TIME_FORMAT;
static {
TIME_FORMAT = NumberFormat.getInstance(Locale.US);
TIME_FORMAT.setMinimumFractionDigits(2);
TIME_FORMAT.setMaximumFractionDigits(2);
TIME_FORMAT.setGroupingUsed(false);
}
private final MappingTrackSelector trackSelector;
private final Timeline.Window window;
private final Timeline.Period period;
private final long startTimeMs;
public EventLogger(MappingTrackSelector trackSelector) {
this.trackSelector = trackSelector;
window = new Timeline.Window();
period = new Timeline.Period();
startTimeMs = SystemClock.elapsedRealtime();
}
// ExoPlayer.EventListener
@Override
public void onLoadingChanged(boolean isLoading) {
Log.d(TAG, "loading [" + isLoading + "]");
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int state) {
Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", "
+ getStateString(state) + "]");
}
@Override
public void onPositionDiscontinuity() {
Log.d(TAG, "positionDiscontinuity");
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
Log.d(TAG, "playbackParameters " + String.format(
"[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch));
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
int periodCount = timeline.getPeriodCount();
int windowCount = timeline.getWindowCount();
Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount);
for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) {
timeline.getPeriod(i, period);
Log.d(TAG, " " + "period [" + getTimeString(period.getDurationMs()) + "]");
}
if (periodCount > MAX_TIMELINE_ITEM_LINES) {
Log.d(TAG, " ...");
}
for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) {
timeline.getWindow(i, window);
Log.d(TAG, " " + "window [" + getTimeString(window.getDurationMs()) + ", "
+ window.isSeekable + ", " + window.isDynamic + "]");
}
if (windowCount > MAX_TIMELINE_ITEM_LINES) {
Log.d(TAG, " ...");
}
Log.d(TAG, "]");
}
@Override
public void onPlayerError(ExoPlaybackException e) {
Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e);
}
@Override
public void onTracksChanged(TrackGroupArray ignored, TrackSelectionArray trackSelections) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo == null) {
Log.d(TAG, "Tracks []");
return;
}
Log.d(TAG, "Tracks [");
// Log tracks associated to renderers.
for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.length; rendererIndex++) {
TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
TrackSelection trackSelection = trackSelections.get(rendererIndex);
if (rendererTrackGroups.length > 0) {
Log.d(TAG, " Renderer:" + rendererIndex + " [");
for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) {
TrackGroup trackGroup = rendererTrackGroups.get(groupIndex);
String adaptiveSupport = getAdaptiveSupportString(trackGroup.length,
mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false));
Log.d(TAG, " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " [");
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
String status = getTrackStatusString(trackSelection, trackGroup, trackIndex);
String formatSupport = getFormatSupportString(
mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
+ Format.toLogString(trackGroup.getFormat(trackIndex))
+ ", supported=" + formatSupport);
}
Log.d(TAG, " ]");
}
// Log metadata for at most one of the tracks selected for the renderer.
if (trackSelection != null) {
for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) {
Metadata metadata = trackSelection.getFormat(selectionIndex).metadata;
if (metadata != null) {
Log.d(TAG, " Metadata [");
printMetadata(metadata, " ");
Log.d(TAG, " ]");
break;
}
}
}
Log.d(TAG, " ]");
}
}
// Log tracks not associated with a renderer.
TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups();
if (unassociatedTrackGroups.length > 0) {
Log.d(TAG, " Renderer:None [");
for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) {
Log.d(TAG, " Group:" + groupIndex + " [");
TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex);
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
String status = getTrackStatusString(false);
String formatSupport = getFormatSupportString(
RendererCapabilities.FORMAT_UNSUPPORTED_TYPE);
Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
+ Format.toLogString(trackGroup.getFormat(trackIndex))
+ ", supported=" + formatSupport);
}
Log.d(TAG, " ]");
}
Log.d(TAG, " ]");
}
Log.d(TAG, "]");
}
// MetadataRenderer.Output
@Override
public void onMetadata(Metadata metadata) {
Log.d(TAG, "onMetadata [");
printMetadata(metadata, " ");
Log.d(TAG, "]");
}
// AudioRendererEventListener
@Override
public void onAudioEnabled(DecoderCounters counters) {
Log.d(TAG, "audioEnabled [" + getSessionTimeString() + "]");
}
@Override
public void onAudioSessionId(int audioSessionId) {
Log.d(TAG, "audioSessionId [" + audioSessionId + "]");
}
@Override
public void onAudioDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs) {
Log.d(TAG, "audioDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]");
}
@Override
public void onAudioInputFormatChanged(Format format) {
Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
+ "]");
}
@Override
public void onAudioDisabled(DecoderCounters counters) {
Log.d(TAG, "audioDisabled [" + getSessionTimeString() + "]");
}
@Override
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", "
+ elapsedSinceLastFeedMs + "]", null);
}
// VideoRendererEventListener
@Override
public void onVideoEnabled(DecoderCounters counters) {
Log.d(TAG, "videoEnabled [" + getSessionTimeString() + "]");
}
@Override
public void onVideoDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs) {
Log.d(TAG, "videoDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]");
}
@Override
public void onVideoInputFormatChanged(Format format) {
Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
+ "]");
}
@Override
public void onVideoDisabled(DecoderCounters counters) {
Log.d(TAG, "videoDisabled [" + getSessionTimeString() + "]");
}
@Override
public void onDroppedFrames(int count, long elapsed) {
Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]");
}
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio) {
// Do nothing.
}
@Override
public void onRenderedFirstFrame(Surface surface) {
Log.d(TAG, "renderedFirstFrame [" + surface + "]");
}
// DefaultDrmSessionManager.EventListener
@Override
public void onDrmSessionManagerError(Exception e) {
printInternalError("drmSessionManagerError", e);
}
@Override
public void onDrmKeysRestored() {
Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]");
}
@Override
public void onDrmKeysRemoved() {
Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]");
}
@Override
public void onDrmKeysLoaded() {
Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]");
}
// ExtractorMediaSource.EventListener
@Override
public void onLoadError(IOException error) {
printInternalError("loadError", error);
}
// AdaptiveMediaSourceEventListener
@Override
public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
long mediaEndTimeMs, long elapsedRealtimeMs) {
// Do nothing.
}
@Override
public void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded,
IOException error, boolean wasCanceled) {
printInternalError("loadError", error);
}
@Override
public void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) {
// Do nothing.
}
@Override
public void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) {
// Do nothing.
}
@Override
public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) {
// Do nothing.
}
@Override
public void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason,
Object trackSelectionData, long mediaTimeMs) {
// Do nothing.
}
// Internal methods
private void printInternalError(String type, Exception e) {
Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
}
private void printMetadata(Metadata metadata, String prefix) {
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof TextInformationFrame) {
TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
Log.d(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id,
textInformationFrame.value));
} else if (entry instanceof UrlLinkFrame) {
UrlLinkFrame urlLinkFrame = (UrlLinkFrame) entry;
Log.d(TAG, prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url));
} else if (entry instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) entry;
Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner));
} else if (entry instanceof GeobFrame) {
GeobFrame geobFrame = (GeobFrame) entry;
Log.d(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s",
geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description));
} else if (entry instanceof ApicFrame) {
ApicFrame apicFrame = (ApicFrame) entry;
Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s",
apicFrame.id, apicFrame.mimeType, apicFrame.description));
} else if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry;
Log.d(TAG, prefix + String.format("%s: language=%s, description=%s", commentFrame.id,
commentFrame.language, commentFrame.description));
} else if (entry instanceof Id3Frame) {
Id3Frame id3Frame = (Id3Frame) entry;
Log.d(TAG, prefix + String.format("%s", id3Frame.id));
} else if (entry instanceof EventMessage) {
EventMessage eventMessage = (EventMessage) entry;
Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s",
eventMessage.schemeIdUri, eventMessage.id, eventMessage.value));
}
}
}
private String getSessionTimeString() {
return getTimeString(SystemClock.elapsedRealtime() - startTimeMs);
}
private static String getTimeString(long timeMs) {
return timeMs == C.TIME_UNSET ? "?" : TIME_FORMAT.format((timeMs) / 1000f);
}
private static 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_READY:
return "R";
default:
return "?";
}
}
private static String getFormatSupportString(int formatSupport) {
switch (formatSupport) {
case RendererCapabilities.FORMAT_HANDLED:
return "YES";
case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:
return "NO_EXCEEDS_CAPABILITIES";
case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE:
return "NO_UNSUPPORTED_TYPE";
case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE:
return "NO";
default:
return "?";
}
}
private static String getAdaptiveSupportString(int trackCount, int adaptiveSupport) {
if (trackCount < 2) {
return "N/A";
}
switch (adaptiveSupport) {
case RendererCapabilities.ADAPTIVE_SEAMLESS:
return "YES";
case RendererCapabilities.ADAPTIVE_NOT_SEAMLESS:
return "YES_NOT_SEAMLESS";
case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED:
return "NO";
default:
return "?";
}
}
private static String getTrackStatusString(TrackSelection selection, TrackGroup group,
int trackIndex) {
return getTrackStatusString(selection != null && selection.getTrackGroup() == group
&& selection.indexOf(trackIndex) != C.INDEX_UNSET);
}
private static String getTrackStatusString(boolean enabled) {
return enabled ? "[X]" : "[ ]";
}
}

View File

@ -1,574 +0,0 @@
/*
* Copyright (C) 2016 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.exoplayer2.demo;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
import com.google.android.exoplayer2.ui.PlaybackControlView;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Util;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.UUID;
/**
* An activity that plays media using {@link SimpleExoPlayer}.
*/
public class PlayerActivity extends Activity implements OnClickListener, ExoPlayer.EventListener,
PlaybackControlView.VisibilityListener {
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
public static final String DRM_LICENSE_URL = "drm_license_url";
public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties";
public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders";
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
public static final String EXTENSION_EXTRA = "extension";
public static final String ACTION_VIEW_LIST =
"com.google.android.exoplayer.demo.action.VIEW_LIST";
public static final String URI_LIST_EXTRA = "uri_list";
public static final String EXTENSION_LIST_EXTRA = "extension_list";
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private static final CookieManager DEFAULT_COOKIE_MANAGER;
static {
DEFAULT_COOKIE_MANAGER = new CookieManager();
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
}
private Handler mainHandler;
private EventLogger eventLogger;
private SimpleExoPlayerView simpleExoPlayerView;
private LinearLayout debugRootView;
private TextView debugTextView;
private Button retryButton;
private DataSource.Factory mediaDataSourceFactory;
private SimpleExoPlayer player;
private DefaultTrackSelector trackSelector;
private TrackSelectionHelper trackSelectionHelper;
private DebugTextViewHelper debugViewHelper;
private boolean needRetrySource;
private TrackGroupArray lastSeenTrackGroupArray;
private boolean shouldAutoPlay;
private int resumeWindow;
private long resumePosition;
// Activity lifecycle
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
shouldAutoPlay = true;
clearResumePosition();
mediaDataSourceFactory = buildDataSourceFactory(true);
mainHandler = new Handler();
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
}
setContentView(R.layout.player_activity);
View rootView = findViewById(R.id.root);
rootView.setOnClickListener(this);
debugRootView = (LinearLayout) findViewById(R.id.controls_root);
debugTextView = (TextView) findViewById(R.id.debug_text_view);
retryButton = (Button) findViewById(R.id.retry_button);
retryButton.setOnClickListener(this);
simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view);
simpleExoPlayerView.setControllerVisibilityListener(this);
simpleExoPlayerView.requestFocus();
}
@Override
public void onNewIntent(Intent intent) {
releasePlayer();
shouldAutoPlay = true;
clearResumePosition();
setIntent(intent);
}
@Override
public void onStart() {
super.onStart();
if (Util.SDK_INT > 23) {
initializePlayer();
}
}
@Override
public void onResume() {
super.onResume();
if ((Util.SDK_INT <= 23 || player == null)) {
initializePlayer();
}
}
@Override
public void onPause() {
super.onPause();
if (Util.SDK_INT <= 23) {
releasePlayer();
}
}
@Override
public void onStop() {
super.onStop();
if (Util.SDK_INT > 23) {
releasePlayer();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initializePlayer();
} else {
showToast(R.string.storage_permission_denied);
finish();
}
}
// Activity input
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Show the controls on any key event.
simpleExoPlayerView.showController();
// If the event was not handled then see if the player view can handle it as a media key event.
return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchMediaKeyEvent(event);
}
// OnClickListener methods
@Override
public void onClick(View view) {
if (view == retryButton) {
initializePlayer();
} else if (view.getParent() == debugRootView) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(),
trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag());
}
}
}
// PlaybackControlView.VisibilityListener implementation
@Override
public void onVisibilityChange(int visibility) {
debugRootView.setVisibility(visibility);
}
// Internal methods
private void initializePlayer() {
Intent intent = getIntent();
boolean needNewPlayer = player == null;
if (needNewPlayer) {
TrackSelection.Factory adaptiveTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory);
lastSeenTrackGroupArray = null;
eventLogger = new EventLogger(trackSelector);
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
if (drmSchemeUuid != null) {
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
try {
drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl,
keyRequestPropertiesArray);
} catch (UnsupportedDrmException e) {
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
showToast(errorStringId);
return;
}
}
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
((DemoApplication) getApplication()).useExtensionRenderers()
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this,
drmSessionManager, extensionRendererMode);
player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
player.addListener(this);
player.addListener(eventLogger);
player.setAudioDebugListener(eventLogger);
player.setVideoDebugListener(eventLogger);
player.setMetadataOutput(eventLogger);
simpleExoPlayerView.setPlayer(player);
player.setPlayWhenReady(shouldAutoPlay);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
}
if (needNewPlayer || needRetrySource) {
String action = intent.getAction();
Uri[] uris;
String[] extensions;
if (ACTION_VIEW.equals(action)) {
uris = new Uri[] {intent.getData()};
extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)};
} else if (ACTION_VIEW_LIST.equals(action)) {
String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
uris = new Uri[uriStrings.length];
for (int i = 0; i < uriStrings.length; i++) {
uris[i] = Uri.parse(uriStrings[i]);
}
extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA);
if (extensions == null) {
extensions = new String[uriStrings.length];
}
} else {
showToast(getString(R.string.unexpected_intent_action, action));
return;
}
if (Util.maybeRequestReadExternalStoragePermission(this, uris)) {
// The player will be reinitialized if the permission is granted.
return;
}
MediaSource[] mediaSources = new MediaSource[uris.length];
for (int i = 0; i < uris.length; i++) {
mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
}
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
: new ConcatenatingMediaSource(mediaSources);
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
if (haveResumePosition) {
player.seekTo(resumeWindow, resumePosition);
}
player.prepare(mediaSource, !haveResumePosition, false);
needRetrySource = false;
updateButtonVisibilities();
}
}
private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
: Util.inferContentType("." + overrideExtension);
switch (type) {
case C.TYPE_SS:
return new SsMediaSource(uri, buildDataSourceFactory(false),
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger);
case C.TYPE_DASH:
return new DashMediaSource(uri, buildDataSourceFactory(false),
new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger);
case C.TYPE_HLS:
return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger);
case C.TYPE_OTHER:
return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(),
mainHandler, eventLogger);
default: {
throw new IllegalStateException("Unsupported type: " + type);
}
}
}
private DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManager(UUID uuid,
String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException {
if (Util.SDK_INT < 18) {
return null;
}
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
buildHttpDataSourceFactory(false));
if (keyRequestPropertiesArray != null) {
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
keyRequestPropertiesArray[i + 1]);
}
}
return new DefaultDrmSessionManager<>(uuid,
FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
}
private void releasePlayer() {
if (player != null) {
debugViewHelper.stop();
debugViewHelper = null;
shouldAutoPlay = player.getPlayWhenReady();
updateResumePosition();
player.release();
player = null;
trackSelector = null;
trackSelectionHelper = null;
eventLogger = null;
}
}
private void updateResumePosition() {
resumeWindow = player.getCurrentWindowIndex();
resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getCurrentPosition())
: C.TIME_UNSET;
}
private void clearResumePosition() {
resumeWindow = C.INDEX_UNSET;
resumePosition = C.TIME_UNSET;
}
/**
* Returns a new DataSource factory.
*
* @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new
* DataSource factory.
* @return A new DataSource factory.
*/
private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
return ((DemoApplication) getApplication())
.buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
}
/**
* Returns a new HttpDataSource factory.
*
* @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new
* DataSource factory.
* @return A new HttpDataSource factory.
*/
private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {
return ((DemoApplication) getApplication())
.buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
}
// ExoPlayer.EventListener implementation
@Override
public void onLoadingChanged(boolean isLoading) {
// Do nothing.
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == ExoPlayer.STATE_ENDED) {
showControls();
}
updateButtonVisibilities();
}
@Override
public void onPositionDiscontinuity() {
if (needRetrySource) {
// This will only occur if the user has performed a seek whilst in the error state. Update the
// resume position so that if the user then retries, playback will resume from the position to
// which they seeked.
updateResumePosition();
}
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
// Do nothing.
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
// Do nothing.
}
@Override
public void onPlayerError(ExoPlaybackException e) {
String errorString = null;
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
Exception cause = e.getRendererException();
if (cause instanceof DecoderInitializationException) {
// Special case for decoder initialization failures.
DecoderInitializationException decoderInitializationException =
(DecoderInitializationException) cause;
if (decoderInitializationException.decoderName == null) {
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
errorString = getString(R.string.error_querying_decoders);
} else if (decoderInitializationException.secureDecoderRequired) {
errorString = getString(R.string.error_no_secure_decoder,
decoderInitializationException.mimeType);
} else {
errorString = getString(R.string.error_no_decoder,
decoderInitializationException.mimeType);
}
} else {
errorString = getString(R.string.error_instantiating_decoder,
decoderInitializationException.decoderName);
}
}
}
if (errorString != null) {
showToast(errorString);
}
needRetrySource = true;
if (isBehindLiveWindow(e)) {
clearResumePosition();
initializePlayer();
} else {
updateResumePosition();
updateButtonVisibilities();
showControls();
}
}
@Override
@SuppressWarnings("ReferenceEquality")
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
updateButtonVisibilities();
if (trackGroups != lastSeenTrackGroupArray) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_video);
}
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_audio);
}
}
lastSeenTrackGroupArray = trackGroups;
}
}
// User controls
private void updateButtonVisibilities() {
debugRootView.removeAllViews();
retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE);
debugRootView.addView(retryButton);
if (player == null) {
return;
}
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo == null) {
return;
}
for (int i = 0; i < mappedTrackInfo.length; i++) {
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
if (trackGroups.length != 0) {
Button button = new Button(this);
int label;
switch (player.getRendererType(i)) {
case C.TRACK_TYPE_AUDIO:
label = R.string.audio;
break;
case C.TRACK_TYPE_VIDEO:
label = R.string.video;
break;
case C.TRACK_TYPE_TEXT:
label = R.string.text;
break;
default:
continue;
}
button.setText(label);
button.setTag(i);
button.setOnClickListener(this);
debugRootView.addView(button, debugRootView.getChildCount() - 1);
}
}
}
private void showControls() {
debugRootView.setVisibility(View.VISIBLE);
}
private void showToast(int messageId) {
showToast(getString(messageId));
}
private void showToast(String message) {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
}
private static boolean isBehindLiveWindow(ExoPlaybackException e) {
if (e.type != ExoPlaybackException.TYPE_SOURCE) {
return false;
}
Throwable cause = e.getSourceException();
while (cause != null) {
if (cause instanceof BehindLiveWindowException) {
return true;
}
cause = cause.getCause();
}
return false;
}
}

View File

@ -1,451 +0,0 @@
/*
* Copyright (C) 2016 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.exoplayer2.demo;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.JsonReader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
* An activity for selecting from a list of samples.
*/
public class SampleChooserActivity extends Activity {
private static final String TAG = "SampleChooserActivity";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sample_chooser_activity);
Intent intent = getIntent();
String dataUri = intent.getDataString();
String[] uris;
if (dataUri != null) {
uris = new String[] {dataUri};
} else {
ArrayList<String> uriList = new ArrayList<>();
AssetManager assetManager = getAssets();
try {
for (String asset : assetManager.list("")) {
if (asset.endsWith(".exolist.json")) {
uriList.add("asset:///" + asset);
}
}
} catch (IOException e) {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
.show();
}
uris = new String[uriList.size()];
uriList.toArray(uris);
Arrays.sort(uris);
}
SampleListLoader loaderTask = new SampleListLoader();
loaderTask.execute(uris);
}
private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) {
if (sawError) {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
.show();
}
ExpandableListView sampleList = (ExpandableListView) findViewById(R.id.sample_list);
sampleList.setAdapter(new SampleAdapter(this, groups));
sampleList.setOnChildClickListener(new OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View view, int groupPosition,
int childPosition, long id) {
onSampleSelected(groups.get(groupPosition).samples.get(childPosition));
return true;
}
});
}
private void onSampleSelected(Sample sample) {
startActivity(sample.buildIntent(this));
}
private final class SampleListLoader extends AsyncTask<String, Void, List<SampleGroup>> {
private boolean sawError;
@Override
protected List<SampleGroup> doInBackground(String... uris) {
List<SampleGroup> result = new ArrayList<>();
Context context = getApplicationContext();
String userAgent = Util.getUserAgent(context, "ExoPlayerDemo");
DataSource dataSource = new DefaultDataSource(context, null, userAgent, false);
for (String uri : uris) {
DataSpec dataSpec = new DataSpec(Uri.parse(uri));
InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
try {
readSampleGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
} catch (Exception e) {
Log.e(TAG, "Error loading sample list: " + uri, e);
sawError = true;
} finally {
Util.closeQuietly(dataSource);
}
}
return result;
}
@Override
protected void onPostExecute(List<SampleGroup> result) {
onSampleGroups(result, sawError);
}
private void readSampleGroups(JsonReader reader, List<SampleGroup> groups) throws IOException {
reader.beginArray();
while (reader.hasNext()) {
readSampleGroup(reader, groups);
}
reader.endArray();
}
private void readSampleGroup(JsonReader reader, List<SampleGroup> groups) throws IOException {
String groupName = "";
ArrayList<Sample> samples = new ArrayList<>();
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
switch (name) {
case "name":
groupName = reader.nextString();
break;
case "samples":
reader.beginArray();
while (reader.hasNext()) {
samples.add(readEntry(reader, false));
}
reader.endArray();
break;
case "_comment":
reader.nextString(); // Ignore.
break;
default:
throw new ParserException("Unsupported name: " + name);
}
}
reader.endObject();
SampleGroup group = getGroup(groupName, groups);
group.samples.addAll(samples);
}
private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
String sampleName = null;
String uri = null;
String extension = null;
UUID drmUuid = null;
String drmLicenseUrl = null;
String[] drmKeyRequestProperties = null;
boolean preferExtensionDecoders = false;
ArrayList<UriSample> playlistSamples = null;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
switch (name) {
case "name":
sampleName = reader.nextString();
break;
case "uri":
uri = reader.nextString();
break;
case "extension":
extension = reader.nextString();
break;
case "drm_scheme":
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
drmUuid = getDrmUuid(reader.nextString());
break;
case "drm_license_url":
Assertions.checkState(!insidePlaylist,
"Invalid attribute on nested item: drm_license_url");
drmLicenseUrl = reader.nextString();
break;
case "drm_key_request_properties":
Assertions.checkState(!insidePlaylist,
"Invalid attribute on nested item: drm_key_request_properties");
ArrayList<String> drmKeyRequestPropertiesList = new ArrayList<>();
reader.beginObject();
while (reader.hasNext()) {
drmKeyRequestPropertiesList.add(reader.nextName());
drmKeyRequestPropertiesList.add(reader.nextString());
}
reader.endObject();
drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]);
break;
case "prefer_extension_decoders":
Assertions.checkState(!insidePlaylist,
"Invalid attribute on nested item: prefer_extension_decoders");
preferExtensionDecoders = reader.nextBoolean();
break;
case "playlist":
Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
playlistSamples = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
playlistSamples.add((UriSample) readEntry(reader, true));
}
reader.endArray();
break;
default:
throw new ParserException("Unsupported attribute name: " + name);
}
}
reader.endObject();
if (playlistSamples != null) {
UriSample[] playlistSamplesArray = playlistSamples.toArray(
new UriSample[playlistSamples.size()]);
return new PlaylistSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties,
preferExtensionDecoders, playlistSamplesArray);
} else {
return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties,
preferExtensionDecoders, uri, extension);
}
}
private SampleGroup getGroup(String groupName, List<SampleGroup> groups) {
for (int i = 0; i < groups.size(); i++) {
if (Util.areEqual(groupName, groups.get(i).title)) {
return groups.get(i);
}
}
SampleGroup group = new SampleGroup(groupName);
groups.add(group);
return group;
}
private UUID getDrmUuid(String typeString) throws ParserException {
switch (Util.toLowerInvariant(typeString)) {
case "widevine":
return C.WIDEVINE_UUID;
case "playready":
return C.PLAYREADY_UUID;
case "cenc":
return C.CLEARKEY_UUID;
default:
try {
return UUID.fromString(typeString);
} catch (RuntimeException e) {
throw new ParserException("Unsupported drm type: " + typeString);
}
}
}
}
private static final class SampleAdapter extends BaseExpandableListAdapter {
private final Context context;
private final List<SampleGroup> sampleGroups;
public SampleAdapter(Context context, List<SampleGroup> sampleGroups) {
this.context = context;
this.sampleGroups = sampleGroups;
}
@Override
public Sample getChild(int groupPosition, int childPosition) {
return getGroup(groupPosition).samples.get(childPosition);
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent,
false);
}
((TextView) view).setText(getChild(groupPosition, childPosition).name);
return view;
}
@Override
public int getChildrenCount(int groupPosition) {
return getGroup(groupPosition).samples.size();
}
@Override
public SampleGroup getGroup(int groupPosition) {
return sampleGroups.get(groupPosition);
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
ViewGroup parent) {
View view = convertView;
if (view == null) {
view = LayoutInflater.from(context).inflate(android.R.layout.simple_expandable_list_item_1,
parent, false);
}
((TextView) view).setText(getGroup(groupPosition).title);
return view;
}
@Override
public int getGroupCount() {
return sampleGroups.size();
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
}
private static final class SampleGroup {
public final String title;
public final List<Sample> samples;
public SampleGroup(String title) {
this.title = title;
this.samples = new ArrayList<>();
}
}
private abstract static class Sample {
public final String name;
public final boolean preferExtensionDecoders;
public final UUID drmSchemeUuid;
public final String drmLicenseUrl;
public final String[] drmKeyRequestProperties;
public Sample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
String[] drmKeyRequestProperties, boolean preferExtensionDecoders) {
this.name = name;
this.drmSchemeUuid = drmSchemeUuid;
this.drmLicenseUrl = drmLicenseUrl;
this.drmKeyRequestProperties = drmKeyRequestProperties;
this.preferExtensionDecoders = preferExtensionDecoders;
}
public Intent buildIntent(Context context) {
Intent intent = new Intent(context, PlayerActivity.class);
intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders);
if (drmSchemeUuid != null) {
intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString());
intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl);
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
}
return intent;
}
}
private static final class UriSample extends Sample {
public final String uri;
public final String extension;
public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri,
String extension) {
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
this.uri = uri;
this.extension = extension;
}
@Override
public Intent buildIntent(Context context) {
return super.buildIntent(context)
.setData(Uri.parse(uri))
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
.setAction(PlayerActivity.ACTION_VIEW);
}
}
private static final class PlaylistSample extends Sample {
public final UriSample[] children;
public PlaylistSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
String[] drmKeyRequestProperties, boolean preferExtensionDecoders,
UriSample... children) {
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
this.children = children;
}
@Override
public Intent buildIntent(Context context) {
String[] uris = new String[children.length];
String[] extensions = new String[children.length];
for (int i = 0; i < children.length; i++) {
uris[i] = children[i].uri;
extensions[i] = children[i].extension;
}
return super.buildIntent(context)
.putExtra(PlayerActivity.URI_LIST_EXTRA, uris)
.putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions)
.setAction(PlayerActivity.ACTION_VIEW_LIST);
}
}
}

View File

@ -1,290 +0,0 @@
/*
* Copyright (C) 2016 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.exoplayer2.demo;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckedTextView;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.SelectionOverride;
import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import java.util.Arrays;
/**
* Helper class for displaying track selection dialogs.
*/
/* package */ final class TrackSelectionHelper implements View.OnClickListener,
DialogInterface.OnClickListener {
private static final TrackSelection.Factory FIXED_FACTORY = new FixedTrackSelection.Factory();
private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory();
private final MappingTrackSelector selector;
private final TrackSelection.Factory adaptiveTrackSelectionFactory;
private MappedTrackInfo trackInfo;
private int rendererIndex;
private TrackGroupArray trackGroups;
private boolean[] trackGroupsAdaptive;
private boolean isDisabled;
private SelectionOverride override;
private CheckedTextView disableView;
private CheckedTextView defaultView;
private CheckedTextView enableRandomAdaptationView;
private CheckedTextView[][] trackViews;
/**
* @param selector The track selector.
* @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null
* if the selection helper should not support adaptive tracks.
*/
public TrackSelectionHelper(MappingTrackSelector selector,
TrackSelection.Factory adaptiveTrackSelectionFactory) {
this.selector = selector;
this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory;
}
/**
* Shows the selection dialog for a given renderer.
*
* @param activity The parent activity.
* @param title The dialog's title.
* @param trackInfo The current track information.
* @param rendererIndex The index of the renderer.
*/
public void showSelectionDialog(Activity activity, CharSequence title, MappedTrackInfo trackInfo,
int rendererIndex) {
this.trackInfo = trackInfo;
this.rendererIndex = rendererIndex;
trackGroups = trackInfo.getTrackGroups(rendererIndex);
trackGroupsAdaptive = new boolean[trackGroups.length];
for (int i = 0; i < trackGroups.length; i++) {
trackGroupsAdaptive[i] = adaptiveTrackSelectionFactory != null
&& trackInfo.getAdaptiveSupport(rendererIndex, i, false)
!= RendererCapabilities.ADAPTIVE_NOT_SUPPORTED
&& trackGroups.get(i).length > 1;
}
isDisabled = selector.getRendererDisabled(rendererIndex);
override = selector.getSelectionOverride(rendererIndex, trackGroups);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(title)
.setView(buildView(builder.getContext()))
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, null)
.create()
.show();
}
@SuppressLint("InflateParams")
private View buildView(Context context) {
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.track_selection_dialog, null);
ViewGroup root = (ViewGroup) view.findViewById(R.id.root);
TypedArray attributeArray = context.getTheme().obtainStyledAttributes(
new int[] {android.R.attr.selectableItemBackground});
int selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0);
attributeArray.recycle();
// View for disabling the renderer.
disableView = (CheckedTextView) inflater.inflate(
android.R.layout.simple_list_item_single_choice, root, false);
disableView.setBackgroundResource(selectableItemBackgroundResourceId);
disableView.setText(R.string.selection_disabled);
disableView.setFocusable(true);
disableView.setOnClickListener(this);
root.addView(disableView);
// View for clearing the override to allow the selector to use its default selection logic.
defaultView = (CheckedTextView) inflater.inflate(
android.R.layout.simple_list_item_single_choice, root, false);
defaultView.setBackgroundResource(selectableItemBackgroundResourceId);
defaultView.setText(R.string.selection_default);
defaultView.setFocusable(true);
defaultView.setOnClickListener(this);
root.addView(inflater.inflate(R.layout.list_divider, root, false));
root.addView(defaultView);
// Per-track views.
boolean haveAdaptiveTracks = false;
trackViews = new CheckedTextView[trackGroups.length][];
for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
TrackGroup group = trackGroups.get(groupIndex);
boolean groupIsAdaptive = trackGroupsAdaptive[groupIndex];
haveAdaptiveTracks |= groupIsAdaptive;
trackViews[groupIndex] = new CheckedTextView[group.length];
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
if (trackIndex == 0) {
root.addView(inflater.inflate(R.layout.list_divider, root, false));
}
int trackViewLayoutId = groupIsAdaptive ? android.R.layout.simple_list_item_multiple_choice
: android.R.layout.simple_list_item_single_choice;
CheckedTextView trackView = (CheckedTextView) inflater.inflate(
trackViewLayoutId, root, false);
trackView.setBackgroundResource(selectableItemBackgroundResourceId);
trackView.setText(DemoUtil.buildTrackName(group.getFormat(trackIndex)));
if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)
== RendererCapabilities.FORMAT_HANDLED) {
trackView.setFocusable(true);
trackView.setTag(Pair.create(groupIndex, trackIndex));
trackView.setOnClickListener(this);
} else {
trackView.setFocusable(false);
trackView.setEnabled(false);
}
trackViews[groupIndex][trackIndex] = trackView;
root.addView(trackView);
}
}
if (haveAdaptiveTracks) {
// View for using random adaptation.
enableRandomAdaptationView = (CheckedTextView) inflater.inflate(
android.R.layout.simple_list_item_multiple_choice, root, false);
enableRandomAdaptationView.setBackgroundResource(selectableItemBackgroundResourceId);
enableRandomAdaptationView.setText(R.string.enable_random_adaptation);
enableRandomAdaptationView.setOnClickListener(this);
root.addView(inflater.inflate(R.layout.list_divider, root, false));
root.addView(enableRandomAdaptationView);
}
updateViews();
return view;
}
private void updateViews() {
disableView.setChecked(isDisabled);
defaultView.setChecked(!isDisabled && override == null);
for (int i = 0; i < trackViews.length; i++) {
for (int j = 0; j < trackViews[i].length; j++) {
trackViews[i][j].setChecked(override != null && override.groupIndex == i
&& override.containsTrack(j));
}
}
if (enableRandomAdaptationView != null) {
boolean enableView = !isDisabled && override != null && override.length > 1;
enableRandomAdaptationView.setEnabled(enableView);
enableRandomAdaptationView.setFocusable(enableView);
if (enableView) {
enableRandomAdaptationView.setChecked(!isDisabled
&& override.factory instanceof RandomTrackSelection.Factory);
}
}
}
// DialogInterface.OnClickListener
@Override
public void onClick(DialogInterface dialog, int which) {
selector.setRendererDisabled(rendererIndex, isDisabled);
if (override != null) {
selector.setSelectionOverride(rendererIndex, trackGroups, override);
} else {
selector.clearSelectionOverrides(rendererIndex);
}
}
// View.OnClickListener
@Override
public void onClick(View view) {
if (view == disableView) {
isDisabled = true;
override = null;
} else if (view == defaultView) {
isDisabled = false;
override = null;
} else if (view == enableRandomAdaptationView) {
setOverride(override.groupIndex, override.tracks, !enableRandomAdaptationView.isChecked());
} else {
isDisabled = false;
@SuppressWarnings("unchecked")
Pair<Integer, Integer> tag = (Pair<Integer, Integer>) view.getTag();
int groupIndex = tag.first;
int trackIndex = tag.second;
if (!trackGroupsAdaptive[groupIndex] || override == null
|| override.groupIndex != groupIndex) {
override = new SelectionOverride(FIXED_FACTORY, groupIndex, trackIndex);
} else {
// The group being modified is adaptive and we already have a non-null override.
boolean isEnabled = ((CheckedTextView) view).isChecked();
int overrideLength = override.length;
if (isEnabled) {
// Remove the track from the override.
if (overrideLength == 1) {
// The last track is being removed, so the override becomes empty.
override = null;
isDisabled = true;
} else {
setOverride(groupIndex, getTracksRemoving(override, trackIndex),
enableRandomAdaptationView.isChecked());
}
} else {
// Add the track to the override.
setOverride(groupIndex, getTracksAdding(override, trackIndex),
enableRandomAdaptationView.isChecked());
}
}
}
// Update the views with the new state.
updateViews();
}
private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) {
TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY
: (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory);
override = new SelectionOverride(factory, group, tracks);
}
// Track array manipulation.
private static int[] getTracksAdding(SelectionOverride override, int addedTrack) {
int[] tracks = override.tracks;
tracks = Arrays.copyOf(tracks, tracks.length + 1);
tracks[tracks.length - 1] = addedTrack;
return tracks;
}
private static int[] getTracksRemoving(SelectionOverride override, int removedTrack) {
int[] tracks = new int[override.length - 1];
int trackCount = 0;
for (int i = 0; i < tracks.length + 1; i++) {
int track = override.tracks[i];
if (track != removedTrack) {
tracks[trackCount++] = track;
}
}
return tracks;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

4
demos/README.md Normal file
View File

@ -0,0 +1,4 @@
# ExoPlayer demos #
This directory contains applications that demonstrate how to use ExoPlayer.
Browse the individual demos and their READMEs to learn more.

4
demos/cast/README.md Normal file
View File

@ -0,0 +1,4 @@
# Cast demo application #
This folder contains a demo application that showcases ExoPlayer integration
with Google Cast.

64
demos/cast/build.gradle Normal file
View File

@ -0,0 +1,64 @@
// Copyright (C) 2017 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 from: '../../constants.gradle'
apply plugin: 'com.android.application'
android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
}
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles = [
"proguard-rules.txt",
getDefaultProguardFile('proguard-android.txt')
]
}
debug {
jniDebuggable = true
}
}
lintOptions {
// The demo app isn't indexed and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
}
dependencies {
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'extension-cast')
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
}
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'

View File

@ -0,0 +1,6 @@
# Proguard rules specific to the Cast demo app.
# Accessed via menu.xml
-keep class androidx.mediarouter.app.MediaRouteActionProvider {
*;
}

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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.exoplayer2.castdemo">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-sdk/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:allowBackup="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/>
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTop" android:label="@string/application_name"
android:theme="@style/Theme.AppCompat">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,104 @@
/*
* Copyright (C) 2017 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.exoplayer2.castdemo;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Utility methods and constants for the Cast demo application. */
/* package */ final class DemoUtil {
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
/** The list of samples available in the cast demo app. */
public static final List<MediaItem> SAMPLES;
static {
ArrayList<MediaItem> samples = new ArrayList<>();
// Clear content.
samples.add(
new MediaItem.Builder()
.setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
.setTitle("Clear DASH: Tears")
.setMimeType(MIME_TYPE_DASH)
.build());
samples.add(
new MediaItem.Builder()
.setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
.setTitle("Clear HLS: Angel one")
.setMimeType(MIME_TYPE_HLS)
.build());
samples.add(
new MediaItem.Builder()
.setUri("https://html5demos.com/assets/dizzy.mp4")
.setTitle("Clear MP4: Dizzy")
.setMimeType(MIME_TYPE_VIDEO_MP4)
.build());
// DRM content.
samples.add(
new MediaItem.Builder()
.setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd"))
.setTitle("Widevine DASH cenc: Tears")
.setMimeType(MIME_TYPE_DASH)
.setDrmConfiguration(
new DrmConfiguration(
C.WIDEVINE_UUID,
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
Collections.emptyMap()))
.build());
samples.add(
new MediaItem.Builder()
.setUri(
Uri.parse(
"https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd"))
.setTitle("Widevine DASH cbc1: Tears")
.setMimeType(MIME_TYPE_DASH)
.setDrmConfiguration(
new DrmConfiguration(
C.WIDEVINE_UUID,
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
Collections.emptyMap()))
.build());
samples.add(
new MediaItem.Builder()
.setUri(
Uri.parse(
"https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd"))
.setTitle("Widevine DASH cbcs: Tears")
.setMimeType(MIME_TYPE_DASH)
.setDrmConfiguration(
new DrmConfiguration(
C.WIDEVINE_UUID,
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
Collections.emptyMap()))
.build());
SAMPLES = Collections.unmodifiableList(samples);
}
private DemoUtil() {}
}

View File

@ -0,0 +1,311 @@
/*
* Copyright (C) 2017 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.exoplayer2.castdemo;
import android.content.Context;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.ColorUtils;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.dynamite.DynamiteModule;
/**
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
* Cast extension.
*/
public class MainActivity extends AppCompatActivity
implements OnClickListener, PlayerManager.Listener {
private PlayerView localPlayerView;
private PlayerControlView castControlView;
private PlayerManager playerManager;
private RecyclerView mediaQueueList;
private MediaQueueListAdapter mediaQueueListAdapter;
private CastContext castContext;
// Activity lifecycle methods.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Getting the cast context later than onStart can cause device discovery not to take place.
try {
castContext = CastContext.getSharedInstance(this);
} catch (RuntimeException e) {
Throwable cause = e.getCause();
while (cause != null) {
if (cause instanceof DynamiteModule.LoadingException) {
setContentView(R.layout.cast_context_error);
return;
}
cause = cause.getCause();
}
// Unknown error. We propagate it.
throw e;
}
setContentView(R.layout.main_activity);
localPlayerView = findViewById(R.id.local_player_view);
localPlayerView.requestFocus();
castControlView = findViewById(R.id.cast_control_view);
mediaQueueList = findViewById(R.id.sample_list);
ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback());
helper.attachToRecyclerView(mediaQueueList);
mediaQueueList.setLayoutManager(new LinearLayoutManager(this));
mediaQueueList.setHasFixedSize(true);
mediaQueueListAdapter = new MediaQueueListAdapter();
findViewById(R.id.add_sample_button).setOnClickListener(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.menu, menu);
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
return true;
}
@Override
public void onResume() {
super.onResume();
if (castContext == null) {
// There is no Cast context to work with. Do nothing.
return;
}
playerManager =
new PlayerManager(
/* listener= */ this,
localPlayerView,
castControlView,
/* context= */ this,
castContext);
mediaQueueList.setAdapter(mediaQueueListAdapter);
}
@Override
public void onPause() {
super.onPause();
if (castContext == null) {
// Nothing to release.
return;
}
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
mediaQueueList.setAdapter(null);
playerManager.release();
playerManager = null;
}
// Activity input.
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// If the event was not handled then see if the player view can handle it.
return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event);
}
@Override
public void onClick(View view) {
new AlertDialog.Builder(this)
.setTitle(R.string.add_samples)
.setView(buildSampleListView())
.setPositiveButton(android.R.string.ok, null)
.create()
.show();
}
// PlayerManager.Listener implementation.
@Override
public void onQueuePositionChanged(int previousIndex, int newIndex) {
if (previousIndex != C.INDEX_UNSET) {
mediaQueueListAdapter.notifyItemChanged(previousIndex);
}
if (newIndex != C.INDEX_UNSET) {
mediaQueueListAdapter.notifyItemChanged(newIndex);
}
}
@Override
public void onUnsupportedTrack(int trackType) {
if (trackType == C.TRACK_TYPE_AUDIO) {
showToast(R.string.error_unsupported_audio);
} else if (trackType == C.TRACK_TYPE_VIDEO) {
showToast(R.string.error_unsupported_video);
} else {
// Do nothing.
}
}
// Internal methods.
private void showToast(int messageId) {
Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show();
}
private View buildSampleListView() {
View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null);
ListView sampleList = dialogList.findViewById(R.id.sample_list);
sampleList.setAdapter(new SampleListAdapter(this));
sampleList.setOnItemClickListener(
(parent, view, position, id) -> {
playerManager.addItem(DemoUtil.SAMPLES.get(position));
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
});
return dialogList;
}
// Internal classes.
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
@Override
public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
TextView v = (TextView) LayoutInflater.from(parent.getContext())
.inflate(android.R.layout.simple_list_item_1, parent, false);
return new QueueItemViewHolder(v);
}
@Override
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
holder.item = playerManager.getItem(position);
TextView view = holder.textView;
view.setText(holder.item.title);
// TODO: Solve coloring using the theme's ColorStateList.
view.setTextColor(
ColorUtils.setAlphaComponent(
view.getCurrentTextColor(),
position == playerManager.getCurrentItemIndex() ? 255 : 100));
}
@Override
public int getItemCount() {
return playerManager.getMediaQueueSize();
}
}
private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback {
private int draggingFromPosition;
private int draggingToPosition;
public RecyclerViewCallback() {
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END);
draggingFromPosition = C.INDEX_UNSET;
draggingToPosition = C.INDEX_UNSET;
}
@Override
public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin,
RecyclerView.ViewHolder target) {
int fromPosition = origin.getAdapterPosition();
int toPosition = target.getAdapterPosition();
if (draggingFromPosition == C.INDEX_UNSET) {
// A drag has started, but changes to the media queue will be reflected in clearView().
draggingFromPosition = fromPosition;
}
draggingToPosition = toPosition;
mediaQueueListAdapter.notifyItemMoved(fromPosition, toPosition);
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
if (playerManager.removeItem(queueItemHolder.item)) {
mediaQueueListAdapter.notifyItemRemoved(position);
// Update whichever item took its place, in case it became the new selected item.
mediaQueueListAdapter.notifyItemChanged(position);
}
}
@Override
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
if (draggingFromPosition != C.INDEX_UNSET) {
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
// A drag has ended. We reflect the media queue change in the player.
if (!playerManager.moveItem(queueItemHolder.item, draggingToPosition)) {
// The move failed. The entire sequence of onMove calls since the drag started needs to be
// invalidated.
mediaQueueListAdapter.notifyDataSetChanged();
}
}
draggingFromPosition = C.INDEX_UNSET;
draggingToPosition = C.INDEX_UNSET;
}
}
private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
public final TextView textView;
public MediaItem item;
public QueueItemViewHolder(TextView textView) {
super(textView);
this.textView = textView;
textView.setOnClickListener(this);
}
@Override
public void onClick(View v) {
playerManager.selectQueueItem(getAdapterPosition());
}
}
private static final class SampleListAdapter extends ArrayAdapter<MediaItem> {
public SampleListAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = super.getView(position, convertView, parent);
((TextView) view).setText(getItem(position).title);
return view;
}
}
}

View File

@ -0,0 +1,459 @@
/*
* Copyright (C) 2019 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.exoplayer2.castdemo;
import android.content.Context;
import android.net.Uri;
import android.view.KeyEvent;
import android.view.View;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter;
import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ext.cast.MediaItemConverter;
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.framework.CastContext;
import java.util.ArrayList;
import java.util.Map;
/** Manages players and an internal media queue for the demo app. */
/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener {
/** Listener for events. */
interface Listener {
/** Called when the currently played item of the media queue changes. */
void onQueuePositionChanged(int previousIndex, int newIndex);
/**
* Called when a track of type {@code trackType} is not supported by the player.
*
* @param trackType One of the {@link C}{@code .TRACK_TYPE_*} constants.
*/
void onUnsupportedTrack(int trackType);
}
private static final String USER_AGENT = "ExoCastDemoPlayer";
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
new DefaultHttpDataSourceFactory(USER_AGENT);
private final PlayerView localPlayerView;
private final PlayerControlView castControlView;
private final DefaultTrackSelector trackSelector;
private final SimpleExoPlayer exoPlayer;
private final CastPlayer castPlayer;
private final ArrayList<MediaItem> mediaQueue;
private final Listener listener;
private final ConcatenatingMediaSource concatenatingMediaSource;
private final MediaItemConverter mediaItemConverter;
private TrackGroupArray lastSeenTrackGroupArray;
private int currentItemIndex;
private Player currentPlayer;
/**
* Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.
*
* @param listener A {@link Listener} for queue position changes.
* @param localPlayerView The {@link PlayerView} for local playback.
* @param castControlView The {@link PlayerControlView} to control remote playback.
* @param context A {@link Context}.
* @param castContext The {@link CastContext}.
*/
public PlayerManager(
Listener listener,
PlayerView localPlayerView,
PlayerControlView castControlView,
Context context,
CastContext castContext) {
this.listener = listener;
this.localPlayerView = localPlayerView;
this.castControlView = castControlView;
mediaQueue = new ArrayList<>();
currentItemIndex = C.INDEX_UNSET;
concatenatingMediaSource = new ConcatenatingMediaSource();
mediaItemConverter = new DefaultMediaItemConverter();
trackSelector = new DefaultTrackSelector(context);
exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build();
exoPlayer.addListener(this);
localPlayerView.setPlayer(exoPlayer);
castPlayer = new CastPlayer(castContext);
castPlayer.addListener(this);
castPlayer.setSessionAvailabilityListener(this);
castControlView.setPlayer(castPlayer);
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
}
// Queue manipulation methods.
/**
* Plays a specified queue item in the current player.
*
* @param itemIndex The index of the item to play.
*/
public void selectQueueItem(int itemIndex) {
setCurrentItem(itemIndex, C.TIME_UNSET, true);
}
/** Returns the index of the currently played item. */
public int getCurrentItemIndex() {
return currentItemIndex;
}
/**
* Appends {@code item} to the media queue.
*
* @param item The {@link MediaItem} to append.
*/
public void addItem(MediaItem item) {
mediaQueue.add(item);
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
if (currentPlayer == castPlayer) {
castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item));
}
}
/** Returns the size of the media queue. */
public int getMediaQueueSize() {
return mediaQueue.size();
}
/**
* Returns the item at the given index in the media queue.
*
* @param position The index of the item.
* @return The item at the given index in the media queue.
*/
public MediaItem getItem(int position) {
return mediaQueue.get(position);
}
/**
* Removes the item at the given index from the media queue.
*
* @param item The item to remove.
* @return Whether the removal was successful.
*/
public boolean removeItem(MediaItem item) {
int itemIndex = mediaQueue.indexOf(item);
if (itemIndex == -1) {
return false;
}
concatenatingMediaSource.removeMediaSource(itemIndex);
if (currentPlayer == castPlayer) {
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
if (castTimeline.getPeriodCount() <= itemIndex) {
return false;
}
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
}
}
mediaQueue.remove(itemIndex);
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
} else if (itemIndex < currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
}
return true;
}
/**
* Moves an item within the queue.
*
* @param item The item to move.
* @param toIndex The target index of the item in the queue.
* @return Whether the item move was successful.
*/
public boolean moveItem(MediaItem item, int toIndex) {
int fromIndex = mediaQueue.indexOf(item);
if (fromIndex == -1) {
return false;
}
// Player update.
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
int periodCount = castTimeline.getPeriodCount();
if (periodCount <= fromIndex || periodCount <= toIndex) {
return false;
}
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
castPlayer.moveItem(elementId, toIndex);
}
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
// Index update.
if (fromIndex == currentItemIndex) {
maybeSetCurrentItemAndNotify(toIndex);
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
}
return true;
}
/**
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
*
* @param event The {@link KeyEvent}.
* @return Whether the event was handled by the target view.
*/
public boolean dispatchKeyEvent(KeyEvent event) {
if (currentPlayer == exoPlayer) {
return localPlayerView.dispatchKeyEvent(event);
} else /* currentPlayer == castPlayer */ {
return castControlView.dispatchKeyEvent(event);
}
}
/** Releases the manager and the players that it holds. */
public void release() {
currentItemIndex = C.INDEX_UNSET;
mediaQueue.clear();
concatenatingMediaSource.clear();
castPlayer.setSessionAvailabilityListener(null);
castPlayer.release();
localPlayerView.setPlayer(null);
exoPlayer.release();
}
// Player.EventListener implementation.
@Override
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
updateCurrentItemIndex();
}
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
updateCurrentItemIndex();
}
@Override
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
updateCurrentItemIndex();
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) {
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
== MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO);
}
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
== MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO);
}
}
lastSeenTrackGroupArray = trackGroups;
}
}
// CastPlayer.SessionAvailabilityListener implementation.
@Override
public void onCastSessionAvailable() {
setCurrentPlayer(castPlayer);
}
@Override
public void onCastSessionUnavailable() {
setCurrentPlayer(exoPlayer);
}
// Internal methods.
private void updateCurrentItemIndex() {
int playbackState = currentPlayer.getPlaybackState();
maybeSetCurrentItemAndNotify(
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
? currentPlayer.getCurrentWindowIndex()
: C.INDEX_UNSET);
}
private void setCurrentPlayer(Player currentPlayer) {
if (this.currentPlayer == currentPlayer) {
return;
}
// View management.
if (currentPlayer == exoPlayer) {
localPlayerView.setVisibility(View.VISIBLE);
castControlView.hide();
} else /* currentPlayer == castPlayer */ {
localPlayerView.setVisibility(View.GONE);
castControlView.show();
}
// Player state management.
long playbackPositionMs = C.TIME_UNSET;
int windowIndex = C.INDEX_UNSET;
boolean playWhenReady = false;
Player previousPlayer = this.currentPlayer;
if (previousPlayer != null) {
// Save state from the previous player.
int playbackState = previousPlayer.getPlaybackState();
if (playbackState != Player.STATE_ENDED) {
playbackPositionMs = previousPlayer.getCurrentPosition();
playWhenReady = previousPlayer.getPlayWhenReady();
windowIndex = previousPlayer.getCurrentWindowIndex();
if (windowIndex != currentItemIndex) {
playbackPositionMs = C.TIME_UNSET;
windowIndex = currentItemIndex;
}
}
previousPlayer.stop(true);
}
this.currentPlayer = currentPlayer;
// Media queue management.
if (currentPlayer == exoPlayer) {
exoPlayer.prepare(concatenatingMediaSource);
}
// Playback transition.
if (windowIndex != C.INDEX_UNSET) {
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
}
}
/**
* Starts playback of the item at the given position.
*
* @param itemIndex The index of the item to play.
* @param positionMs The position at which playback should start.
* @param playWhenReady Whether the player should proceed when ready to do so.
*/
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
maybeSetCurrentItemAndNotify(itemIndex);
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
for (int i = 0; i < items.length; i++) {
items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i));
}
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
} else {
currentPlayer.seekTo(itemIndex, positionMs);
currentPlayer.setPlayWhenReady(playWhenReady);
}
}
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
if (this.currentItemIndex != currentItemIndex) {
int oldIndex = this.currentItemIndex;
this.currentItemIndex = currentItemIndex;
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
}
}
private MediaSource buildMediaSource(MediaItem item) {
Uri uri = item.uri;
String mimeType = item.mimeType;
if (mimeType == null) {
throw new IllegalArgumentException("mimeType is required");
}
DrmSessionManager<ExoMediaCrypto> drmSessionManager =
DrmSessionManager.getDummyDrmSessionManager();
MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration;
if (drmConfiguration != null && Util.SDK_INT >= 18) {
String licenseServerUrl =
drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : "";
HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY);
for (Map.Entry<String, String> requestHeader : drmConfiguration.requestHeaders.entrySet()) {
drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue());
}
drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setMultiSession(/* multiSession= */ true)
.setUuidAndExoMediaDrmProvider(
drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback);
}
MediaSource createdMediaSource;
switch (mimeType) {
case DemoUtil.MIME_TYPE_SS:
createdMediaSource =
new SsMediaSource.Factory(DATA_SOURCE_FACTORY)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
break;
case DemoUtil.MIME_TYPE_DASH:
createdMediaSource =
new DashMediaSource.Factory(DATA_SOURCE_FACTORY)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
break;
case DemoUtil.MIME_TYPE_HLS:
createdMediaSource =
new HlsMediaSource.Factory(DATA_SOURCE_FACTORY)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
break;
case DemoUtil.MIME_TYPE_VIDEO_MP4:
createdMediaSource =
new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
break;
default:
throw new IllegalArgumentException("mimeType is unsupported: " + mimeType);
}
return createdMediaSource;
}
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24.0dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24.0dp" >
<path
android:fillColor="#FFFFFFFF"
android:pathData="M18,13h-5v5c0,0.55 -0.45,1 -1,1h0c-0.55,0 -1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1v0c0,-0.55 0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1h0c0.55,0 1,0.45 1,1v5h5c0.55,0 1,0.45 1,1v0C19,12.55 18.55,13 18,13z"/>
</vector>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="20sp"
android:text="@string/cast_context_error"/>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@android:color/black"
app:repeat_toggle_modes="all|one"/>
<RelativeLayout android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView android:id="@+id/sample_list"
android:choiceMode="singleChoice"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:fadeScrollbars="false"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/add_sample_button"
android:src="@drawable/ic_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_margin="16dp"
android:contentDescription="@string/add_samples"/>
</RelativeLayout>
<com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:repeat_toggle_modes="all|one"
app:show_timeout="-1"/>
</LinearLayout>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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">
<ListView android:id="@+id/sample_list"
android:layout_width="match_parent"
android:layout_height="250dp"
android:fadeScrollbars="false"/>
</LinearLayout>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/media_route_menu_item"
android:title="@string/media_route_menu_title"
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:showAsAction="always" />
</menu>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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>
<string name="application_name">Exo Cast Demo</string>
<string name="media_route_menu_title">Cast</string>
<string name="add_samples">Add samples</string>
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
<string name="error_unsupported_video">Media includes video tracks, but none are playable by this device</string>
<string name="error_unsupported_audio">Media includes audio tracks, but none are playable by this device</string>
</resources>

View File

@ -1,5 +1,5 @@
# Demo application # # ExoPlayer main demo #
This folder contains a demo application that uses ExoPlayer to play a number This is the main ExoPlayer demo application. It uses ExoPlayer to play a number
of test streams. It can be used as a starting point or reference project when of test streams. It can be used as a starting point or reference project when
developing other applications that make use of the ExoPlayer library. developing other applications that make use of the ExoPlayer library.

82
demos/main/build.gradle Normal file
View File

@ -0,0 +1,82 @@
// Copyright (C) 2016 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 from: '../../constants.gradle'
apply plugin: 'com.android.application'
android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
}
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles = [
"proguard-rules.txt",
getDefaultProguardFile('proguard-android.txt')
]
}
debug {
jniDebuggable = true
}
}
lintOptions {
// The demo app isn't indexed, doesn't have translations, and has a
// banner for AndroidTV that's only in xhdpi density.
disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
}
flavorDimensions "extensions"
productFlavors {
noExtensions {
dimension "extensions"
}
withExtensions {
dimension "extensions"
}
}
}
dependencies {
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'com.google.android.material:material:1.0.0'
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui')
withExtensionsImplementation project(path: modulePrefix + 'extension-av1')
withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
withExtensionsImplementation project(path: modulePrefix + 'extension-ima')
withExtensionsImplementation project(path: modulePrefix + 'extension-opus')
withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')
withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp')
}
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'

View File

@ -0,0 +1,7 @@
# Proguard rules specific to the main demo app.
# Constructor accessed via reflection in PlayerActivity
-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader
-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader {
<init>(android.content.Context, android.net.Uri);
}

View File

@ -15,15 +15,18 @@
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo" xmlns:tools="http://schemas.android.com/tools"
android:versionCode="2404" package="com.google.android.exoplayer2.demo">
android:versionName="2.4.4">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-feature android:name="android.software.leanback" android:required="false"/> <uses-feature android:name="android.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/> <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="24"/> <uses-sdk/>
<application <application
android:label="@string/application_name" android:label="@string/application_name"
@ -31,11 +34,14 @@
android:banner="@drawable/ic_banner" android:banner="@drawable/ic_banner"
android:largeHeap="true" android:largeHeap="true"
android:allowBackup="false" android:allowBackup="false"
android:name="com.google.android.exoplayer2.demo.DemoApplication"> android:requestLegacyExternalStorage="true"
android:name="com.google.android.exoplayer2.demo.DemoApplication"
tools:ignore="UnusedAttribute">
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity" <activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:label="@string/application_name"> android:label="@string/application_name"
android:theme="@style/Theme.AppCompat">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
@ -75,6 +81,18 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name="com.google.android.exoplayer2.demo.DemoDownloadService"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>
<service android:name="com.google.android.exoplayer2.scheduler.PlatformScheduler$PlatformSchedulerService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,620 @@
[
{
"name": "YouTube DASH",
"samples": [
{
"name": "Google Glass (MP4,H264)",
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
"extension": "mpd"
},
{
"name": "Google Play (MP4,H264)",
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0",
"extension": "mpd"
},
{
"name": "Google Glass (WebM,VP9)",
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0",
"extension": "mpd"
},
{
"name": "Google Play (WebM,VP9)",
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0",
"extension": "mpd"
}
]
},
{
"name": "Widevine DASH Policy Tests (GTS)",
"samples": [
{
"name": "WV: HDCP not specified",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1c&provider=widevine_test"
},
{
"name": "WV: HDCP not required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=48fcc369939ac96c&provider=widevine_test"
},
{
"name": "WV: HDCP required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=e06c39f1151da3df&provider=widevine_test"
},
{
"name": "WV: Secure video path required (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
},
{
"name": "WV: Secure video path required (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
},
{
"name": "WV: Secure video path required (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test"
},
{
"name": "WV: HDCP + secure video path required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=efd045b1eb61888a&provider=widevine_test"
},
{
"name": "WV: 30s license duration (fails at ~30s)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=f9a34cab7b05881a&provider=widevine_test"
}
]
},
{
"name": "Widevine HDCP Capabilities Tests",
"samples": [
{
"name": "WV: HDCP: None (not required)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_None&provider=widevine_test"
},
{
"name": "WV: HDCP: 1.0 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V1&provider=widevine_test"
},
{
"name": "WV: HDCP: 2.0 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2&provider=widevine_test"
},
{
"name": "WV: HDCP: 2.1 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_1&provider=widevine_test"
},
{
"name": "WV: HDCP: 2.2 required",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_2&provider=widevine_test"
},
{
"name": "WV: HDCP: No digital output",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_NO_DIGTAL_OUTPUT&provider=widevine_test"
}
]
},
{
"name": "Widevine DASH: MP4,H264",
"samples": [
{
"name": "WV: Clear SD & HD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
},
{
"name": "WV: Clear SD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd"
},
{
"name": "WV: Clear HD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd"
},
{
"name": "WV: Clear UHD (MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
},
{
"name": "WV: Secure SD & HD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD & HD (cbc1,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (cbc1,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (cbc1,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (cbc1,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD & HD (cbcs,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (cbcs,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (cbcs,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (cbcs,MP4,H264)",
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure and Clear SD & HD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
"drm_session_for_clear_types": ["audio", "video"]
}
]
},
{
"name": "Widevine DASH: WebM,VP9",
"samples": [
{
"name": "WV: Clear SD & HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd"
},
{
"name": "WV: Clear SD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd"
},
{
"name": "WV: Clear HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd"
},
{
"name": "WV: Clear UHD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd"
},
{
"name": "WV: Secure Fullsample SD & HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Fullsample SD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Fullsample HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Fullsample UHD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample SD & HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample SD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample UHD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}
]
},
{
"name": "Widevine DASH: MP4,H265",
"samples": [
{
"name": "WV: Clear SD & HD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd"
},
{
"name": "WV: Clear SD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd"
},
{
"name": "WV: Clear HD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd"
},
{
"name": "WV: Clear UHD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd"
},
{
"name": "WV: Secure SD & HD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure SD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure HD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure UHD (MP4,H265)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}
]
},
{
"name": "SmoothStreaming",
"samples": [
{
"name": "Super speed",
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
},
{
"name": "Super speed (PlayReady)",
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
"drm_scheme": "playready"
}
]
},
{
"name": "HLS",
"samples": [
{
"name": "Apple 4x3 basic stream",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8"
},
{
"name": "Apple 16x9 basic stream",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
},
{
"name": "Apple master playlist advanced (TS)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8"
},
{
"name": "Apple master playlist advanced (fMP4)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
},
{
"name": "Apple TS media playlist",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8"
},
{
"name": "Apple AAC media playlist",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
}
]
},
{
"name": "Misc",
"samples": [
{
"name": "Dizzy (MP4)",
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"name": "Apple 10s (AAC)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
},
{
"name": "Apple 10s (TS)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
},
{
"name": "Android screens (MKV)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
},
{
"name": "Screens 360p video (WebM,VP9)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
},
{
"name": "Screens 480p video (FMP4,H264)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4"
},
{
"name": "Screens 1080p video (FMP4,H264)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4"
},
{
"name": "Screens audio (FMP4)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
},
{
"name": "Google Play (MP3)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
},
{
"name": "Google Play (Ogg/Vorbis)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
},
{
"name": "Google Play (FLAC)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/flac/play.flac"
},
{
"name": "Big Buck Bunny video (FLV)",
"uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
},
{
"name": "Big Buck Bunny 480p video (MP4,AV1)",
"uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4"
}
]
},
{
"name": "Playlists",
"samples": [
{
"name": "Cats -> Dogs",
"playlist": [
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
}
]
},
{
"name": "Audio -> Video -> Audio",
"playlist": [
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
},
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
},
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
}
]
},
{
"name": "Clear -> Enc -> Clear -> Enc -> Enc",
"playlist": [
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}
]
}
]
},
{
"name": "IMA sample ad tags",
"samples": [
{
"name": "Single inline linear",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
},
{
"name": "Single skippable inline",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator="
},
{
"name": "Single redirect linear",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirectlinear&correlator="
},
{
"name": "Single redirect error",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&nofb=1&correlator="
},
{
"name": "Single redirect broken (fallback)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&correlator="
},
{
"name": "VMAP pre-roll",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll + bumper",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP post-roll",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonly&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP post-roll + bumper",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonlybumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-, mid- and post-rolls, single ads",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpod&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpodbumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
},
{
"name": "VMAP empty midroll",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll"
},
{
"name": "VMAP full, empty, full midrolls",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2"
}
]
},
{
"name": "360",
"samples": [
{
"name": "Congo (360 top-bottom stereo)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
"spherical_stereo_mode": "top_bottom"
},
{
"name": "Sphericalv2 (180 top-bottom stereo)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4",
"spherical_stereo_mode": "top_bottom"
},
{
"name": "Iceland (360 top-bottom stereo ts)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts",
"spherical_stereo_mode": "top_bottom"
}
]
},
{
"name": "Subtitles",
"samples": [
{
"name": "TTML",
"uri": "https://html5demos.com/assets/dizzy.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml",
"subtitle_mime_type": "application/ttml+xml",
"subtitle_language": "en"
},
{
"name": "SSA/ASS position & alignment",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ssa/test-subs-position.ass",
"subtitle_mime_type": "text/x-ssa",
"subtitle_language": "en"
}
]
}
]

View File

@ -0,0 +1,173 @@
/*
* Copyright (C) 2016 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.exoplayer2.demo;
import android.app.Application;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
/**
* Placeholder application to facilitate overriding Application methods for debugging and testing.
*/
public class DemoApplication extends Application {
private static final String TAG = "DemoApplication";
private static final String DOWNLOAD_ACTION_FILE = "actions";
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
protected String userAgent;
private DatabaseProvider databaseProvider;
private File downloadDirectory;
private Cache downloadCache;
private DownloadManager downloadManager;
private DownloadTracker downloadTracker;
@Override
public void onCreate() {
super.onCreate();
userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
}
/** Returns a {@link DataSource.Factory}. */
public DataSource.Factory buildDataSourceFactory() {
DefaultDataSourceFactory upstreamFactory =
new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
}
/** Returns a {@link HttpDataSource.Factory}. */
public HttpDataSource.Factory buildHttpDataSourceFactory() {
return new DefaultHttpDataSourceFactory(userAgent);
}
/** Returns whether extension renderers should be used. */
public boolean useExtensionRenderers() {
return "withExtensions".equals(BuildConfig.FLAVOR);
}
public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
@DefaultRenderersFactory.ExtensionRendererMode
int extensionRendererMode =
useExtensionRenderers()
? (preferExtensionRenderer
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
return new DefaultRenderersFactory(/* context= */ this)
.setExtensionRendererMode(extensionRendererMode);
}
public DownloadManager getDownloadManager() {
initDownloadManager();
return downloadManager;
}
public DownloadTracker getDownloadTracker() {
initDownloadManager();
return downloadTracker;
}
protected synchronized Cache getDownloadCache() {
if (downloadCache == null) {
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
downloadCache =
new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider());
}
return downloadCache;
}
private synchronized void initDownloadManager() {
if (downloadManager == null) {
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());
upgradeActionFile(
DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
upgradeActionFile(
DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
DownloaderConstructorHelper downloaderConstructorHelper =
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
downloadManager =
new DownloadManager(
this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
downloadTracker =
new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
}
}
private void upgradeActionFile(
String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {
try {
ActionFileUpgradeUtil.upgradeAndDelete(
new File(getDownloadDirectory(), fileName),
/* downloadIdProvider= */ null,
downloadIndex,
/* deleteOnFailure= */ true,
addNewDownloadsAsCompleted);
} catch (IOException e) {
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
}
}
private DatabaseProvider getDatabaseProvider() {
if (databaseProvider == null) {
databaseProvider = new ExoDatabaseProvider(this);
}
return databaseProvider;
}
private File getDownloadDirectory() {
if (downloadDirectory == null) {
downloadDirectory = getExternalFilesDir(null);
if (downloadDirectory == null) {
downloadDirectory = getFilesDir();
}
}
return downloadDirectory;
}
protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
DataSource.Factory upstreamFactory, Cache cache) {
return new CacheDataSourceFactory(
cache,
upstreamFactory,
new FileDataSource.Factory(),
/* cacheWriteDataSinkFactory= */ null,
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
/* eventListener= */ null);
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright (C) 2017 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.exoplayer2.demo;
import android.app.Notification;
import android.content.Context;
import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.scheduler.PlatformScheduler;
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
import com.google.android.exoplayer2.util.NotificationUtil;
import com.google.android.exoplayer2.util.Util;
import java.util.List;
/** A service for downloading media. */
public class DemoDownloadService extends DownloadService {
private static final String CHANNEL_ID = "download_channel";
private static final int JOB_ID = 1;
private static final int FOREGROUND_NOTIFICATION_ID = 1;
private DownloadNotificationHelper notificationHelper;
public DemoDownloadService() {
super(
FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
CHANNEL_ID,
R.string.exo_download_notification_channel_name,
/* channelDescriptionResourceId= */ 0);
}
@Override
public void onCreate() {
super.onCreate();
notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID);
}
@Override
protected DownloadManager getDownloadManager() {
DownloadManager downloadManager = ((DemoApplication) getApplication()).getDownloadManager();
// This will only happen once, because getDownloadManager is guaranteed to be called only once
// in the life cycle of the process.
downloadManager.addListener(
new TerminalStateNotificationHelper(
this, notificationHelper, FOREGROUND_NOTIFICATION_ID + 1));
return downloadManager;
}
@Override
protected PlatformScheduler getScheduler() {
return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null;
}
@Override
protected Notification getForegroundNotification(List<Download> downloads) {
return notificationHelper.buildProgressNotification(
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
}
/**
* Creates and displays notifications for downloads when they complete or fail.
*
* <p>This helper will outlive the lifespan of a single instance of {@link DemoDownloadService}.
* It is static to avoid leaking the first {@link DemoDownloadService} instance.
*/
private static final class TerminalStateNotificationHelper implements DownloadManager.Listener {
private final Context context;
private final DownloadNotificationHelper notificationHelper;
private int nextNotificationId;
public TerminalStateNotificationHelper(
Context context, DownloadNotificationHelper notificationHelper, int firstNotificationId) {
this.context = context.getApplicationContext();
this.notificationHelper = notificationHelper;
nextNotificationId = firstNotificationId;
}
@Override
public void onDownloadChanged(DownloadManager manager, Download download) {
Notification notification;
if (download.state == Download.STATE_COMPLETED) {
notification =
notificationHelper.buildDownloadCompletedNotification(
R.drawable.ic_download_done,
/* contentIntent= */ null,
Util.fromUtf8Bytes(download.request.data));
} else if (download.state == Download.STATE_FAILED) {
notification =
notificationHelper.buildDownloadFailedNotification(
R.drawable.ic_download_done,
/* contentIntent= */ null,
Util.fromUtf8Bytes(download.request.data));
} else {
return;
}
NotificationUtil.setNotification(context, nextNotificationId++, notification);
}
}
}

View File

@ -0,0 +1,274 @@
/*
* Copyright (C) 2017 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.exoplayer2.demo;
import android.content.Context;
import android.content.DialogInterface;
import android.net.Uri;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.offline.DownloadCursor;
import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.DownloadIndex;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArraySet;
/** Tracks media that has been downloaded. */
public class DownloadTracker {
/** Listens for changes in the tracked downloads. */
public interface Listener {
/** Called when the tracked downloads changed. */
void onDownloadsChanged();
}
private static final String TAG = "DownloadTracker";
private final Context context;
private final DataSource.Factory dataSourceFactory;
private final CopyOnWriteArraySet<Listener> listeners;
private final HashMap<Uri, Download> downloads;
private final DownloadIndex downloadIndex;
private final DefaultTrackSelector.Parameters trackSelectorParameters;
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
public DownloadTracker(
Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
this.context = context.getApplicationContext();
this.dataSourceFactory = dataSourceFactory;
listeners = new CopyOnWriteArraySet<>();
downloads = new HashMap<>();
downloadIndex = downloadManager.getDownloadIndex();
trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context);
downloadManager.addListener(new DownloadManagerListener());
loadDownloads();
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
public boolean isDownloaded(Uri uri) {
Download download = downloads.get(uri);
return download != null && download.state != Download.STATE_FAILED;
}
public DownloadRequest getDownloadRequest(Uri uri) {
Download download = downloads.get(uri);
return download != null && download.state != Download.STATE_FAILED ? download.request : null;
}
public void toggleDownload(
FragmentManager fragmentManager,
String name,
Uri uri,
String extension,
RenderersFactory renderersFactory) {
Download download = downloads.get(uri);
if (download != null) {
DownloadService.sendRemoveDownload(
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
} else {
if (startDownloadDialogHelper != null) {
startDownloadDialogHelper.release();
}
startDownloadDialogHelper =
new StartDownloadDialogHelper(
fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name);
}
}
private void loadDownloads() {
try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) {
while (loadedDownloads.moveToNext()) {
Download download = loadedDownloads.getDownload();
downloads.put(download.request.uri, download);
}
} catch (IOException e) {
Log.w(TAG, "Failed to query downloads", e);
}
}
private DownloadHelper getDownloadHelper(
Uri uri, String extension, RenderersFactory renderersFactory) {
int type = Util.inferContentType(uri, extension);
switch (type) {
case C.TYPE_DASH:
return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_SS:
return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_HLS:
return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_OTHER:
return DownloadHelper.forProgressive(context, uri);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
}
private class DownloadManagerListener implements DownloadManager.Listener {
@Override
public void onDownloadChanged(DownloadManager downloadManager, Download download) {
downloads.put(download.request.uri, download);
for (Listener listener : listeners) {
listener.onDownloadsChanged();
}
}
@Override
public void onDownloadRemoved(DownloadManager downloadManager, Download download) {
downloads.remove(download.request.uri);
for (Listener listener : listeners) {
listener.onDownloadsChanged();
}
}
}
private final class StartDownloadDialogHelper
implements DownloadHelper.Callback,
DialogInterface.OnClickListener,
DialogInterface.OnDismissListener {
private final FragmentManager fragmentManager;
private final DownloadHelper downloadHelper;
private final String name;
private TrackSelectionDialog trackSelectionDialog;
private MappedTrackInfo mappedTrackInfo;
public StartDownloadDialogHelper(
FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) {
this.fragmentManager = fragmentManager;
this.downloadHelper = downloadHelper;
this.name = name;
downloadHelper.prepare(this);
}
public void release() {
downloadHelper.release();
if (trackSelectionDialog != null) {
trackSelectionDialog.dismiss();
}
}
// DownloadHelper.Callback implementation.
@Override
public void onPrepared(DownloadHelper helper) {
if (helper.getPeriodCount() == 0) {
Log.d(TAG, "No periods found. Downloading entire stream.");
startDownload();
downloadHelper.release();
return;
}
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
Log.d(TAG, "No dialog content. Downloading entire stream.");
startDownload();
downloadHelper.release();
return;
}
trackSelectionDialog =
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
/* titleId= */ R.string.exo_download_description,
mappedTrackInfo,
trackSelectorParameters,
/* allowAdaptiveSelections =*/ false,
/* allowMultipleOverrides= */ true,
/* onClickListener= */ this,
/* onDismissListener= */ this);
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
}
@Override
public void onPrepareError(DownloadHelper helper, IOException e) {
Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
Log.e(
TAG,
e instanceof DownloadHelper.LiveContentUnsupportedException
? "Downloading live content unsupported"
: "Failed to start download",
e);
}
// DialogInterface.OnClickListener implementation.
@Override
public void onClick(DialogInterface dialog, int which) {
for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) {
downloadHelper.clearTrackSelections(periodIndex);
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) {
downloadHelper.addTrackSelectionForSingleRenderer(
periodIndex,
/* rendererIndex= */ i,
trackSelectorParameters,
trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
}
}
}
DownloadRequest downloadRequest = buildDownloadRequest();
if (downloadRequest.streamKeys.isEmpty()) {
// All tracks were deselected in the dialog. Don't start the download.
return;
}
startDownload(downloadRequest);
}
// DialogInterface.OnDismissListener implementation.
@Override
public void onDismiss(DialogInterface dialogInterface) {
trackSelectionDialog = null;
downloadHelper.release();
}
// Internal methods.
private void startDownload() {
startDownload(buildDownloadRequest());
}
private void startDownload(DownloadRequest downloadRequest) {
DownloadService.sendAddDownload(
context, DemoDownloadService.class, downloadRequest, /* foreground= */ false);
}
private DownloadRequest buildDownloadRequest() {
return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name));
}
}
}

View File

@ -0,0 +1,764 @@
/*
* Copyright (C) 2016 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.exoplayer2.demo;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.MediaDrm;
import android.net.Uri;
import android.os.Bundle;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.demo.Sample.UriSample;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.MediaDrmCallback;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.EventLogger;
import com.google.android.exoplayer2.util.Util;
import java.lang.reflect.Constructor;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** An activity that plays media using {@link SimpleExoPlayer}. */
public class PlayerActivity extends AppCompatActivity
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
// Activity extras.
public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
// Actions.
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
public static final String ACTION_VIEW_LIST =
"com.google.android.exoplayer.demo.action.VIEW_LIST";
// Player configuration extras.
public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
public static final String ABR_ALGORITHM_DEFAULT = "default";
public static final String ABR_ALGORITHM_RANDOM = "random";
// Media item configuration extras.
public static final String URI_EXTRA = "uri";
public static final String EXTENSION_EXTRA = "extension";
public static final String IS_LIVE_EXTRA = "is_live";
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
public static final String DRM_SESSION_FOR_CLEAR_TYPES_EXTRA = "drm_session_for_clear_types";
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
public static final String TUNNELING_EXTRA = "tunneling";
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
public static final String SUBTITLE_URI_EXTRA = "subtitle_uri";
public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type";
public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language";
// For backwards compatibility only.
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
// Saved instance state keys.
private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
private static final String KEY_WINDOW = "window";
private static final String KEY_POSITION = "position";
private static final String KEY_AUTO_PLAY = "auto_play";
private static final CookieManager DEFAULT_COOKIE_MANAGER;
static {
DEFAULT_COOKIE_MANAGER = new CookieManager();
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
}
private PlayerView playerView;
private LinearLayout debugRootView;
private Button selectTracksButton;
private TextView debugTextView;
private boolean isShowingTrackSelectionDialog;
private DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player;
private List<MediaSource> mediaSources;
private DefaultTrackSelector trackSelector;
private DefaultTrackSelector.Parameters trackSelectorParameters;
private DebugTextViewHelper debugViewHelper;
private TrackGroupArray lastSeenTrackGroupArray;
private boolean startAutoPlay;
private int startWindow;
private long startPosition;
// Fields used only for ad playback. The ads loader is loaded via reflection.
private AdsLoader adsLoader;
private Uri loadedAdTagUri;
// Activity lifecycle
@Override
public void onCreate(Bundle savedInstanceState) {
Intent intent = getIntent();
String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
if (sphericalStereoMode != null) {
setTheme(R.style.PlayerTheme_Spherical);
}
super.onCreate(savedInstanceState);
dataSourceFactory = buildDataSourceFactory();
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
}
setContentView(R.layout.player_activity);
debugRootView = findViewById(R.id.controls_root);
debugTextView = findViewById(R.id.debug_text_view);
selectTracksButton = findViewById(R.id.select_tracks_button);
selectTracksButton.setOnClickListener(this);
playerView = findViewById(R.id.player_view);
playerView.setControllerVisibilityListener(this);
playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
playerView.requestFocus();
if (sphericalStereoMode != null) {
int stereoMode;
if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
stereoMode = C.STEREO_MODE_MONO;
} else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
stereoMode = C.STEREO_MODE_TOP_BOTTOM;
} else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
} else {
showToast(R.string.error_unrecognized_stereo_mode);
finish();
return;
}
((SphericalGLSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode);
}
if (savedInstanceState != null) {
trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS);
startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY);
startWindow = savedInstanceState.getInt(KEY_WINDOW);
startPosition = savedInstanceState.getLong(KEY_POSITION);
} else {
DefaultTrackSelector.ParametersBuilder builder =
new DefaultTrackSelector.ParametersBuilder(/* context= */ this);
boolean tunneling = intent.getBooleanExtra(TUNNELING_EXTRA, false);
if (Util.SDK_INT >= 21 && tunneling) {
builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this));
}
trackSelectorParameters = builder.build();
clearStartPosition();
}
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
releasePlayer();
releaseAdsLoader();
clearStartPosition();
setIntent(intent);
}
@Override
public void onStart() {
super.onStart();
if (Util.SDK_INT > 23) {
initializePlayer();
if (playerView != null) {
playerView.onResume();
}
}
}
@Override
public void onResume() {
super.onResume();
if (Util.SDK_INT <= 23 || player == null) {
initializePlayer();
if (playerView != null) {
playerView.onResume();
}
}
}
@Override
public void onPause() {
super.onPause();
if (Util.SDK_INT <= 23) {
if (playerView != null) {
playerView.onPause();
}
releasePlayer();
}
}
@Override
public void onStop() {
super.onStop();
if (Util.SDK_INT > 23) {
if (playerView != null) {
playerView.onPause();
}
releasePlayer();
}
}
@Override
public void onDestroy() {
super.onDestroy();
releaseAdsLoader();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (grantResults.length == 0) {
// Empty results are triggered if a permission is requested while another request was already
// pending and can be safely ignored in this case.
return;
}
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initializePlayer();
} else {
showToast(R.string.storage_permission_denied);
finish();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
updateTrackSelectorParameters();
updateStartPosition();
outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);
outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay);
outState.putInt(KEY_WINDOW, startWindow);
outState.putLong(KEY_POSITION, startPosition);
}
// Activity input
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// See whether the player view wants to handle media or DPAD keys events.
return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
}
// OnClickListener methods
@Override
public void onClick(View view) {
if (view == selectTracksButton
&& !isShowingTrackSelectionDialog
&& TrackSelectionDialog.willHaveContent(trackSelector)) {
isShowingTrackSelectionDialog = true;
TrackSelectionDialog trackSelectionDialog =
TrackSelectionDialog.createForTrackSelector(
trackSelector,
/* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false);
trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null);
}
}
// PlaybackControlView.PlaybackPreparer implementation
@Override
public void preparePlayback() {
player.retry();
}
// PlaybackControlView.VisibilityListener implementation
@Override
public void onVisibilityChange(int visibility) {
debugRootView.setVisibility(visibility);
}
// Internal methods
private void initializePlayer() {
if (player == null) {
Intent intent = getIntent();
mediaSources = createTopLevelMediaSources(intent);
if (mediaSources.isEmpty()) {
return;
}
TrackSelection.Factory trackSelectionFactory;
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
trackSelectionFactory = new AdaptiveTrackSelection.Factory();
} else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
trackSelectionFactory = new RandomTrackSelection.Factory();
} else {
showToast(R.string.error_unrecognized_abr_algorithm);
finish();
return;
}
boolean preferExtensionDecoders =
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
RenderersFactory renderersFactory =
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory);
trackSelector.setParameters(trackSelectorParameters);
lastSeenTrackGroupArray = null;
player =
new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory)
.setTrackSelector(trackSelector)
.build();
player.addListener(new PlayerEventListener());
player.setPlayWhenReady(startAutoPlay);
player.addAnalyticsListener(new EventLogger(trackSelector));
playerView.setPlayer(player);
playerView.setPlaybackPreparer(this);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
if (adsLoader != null) {
adsLoader.setPlayer(player);
}
}
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
if (haveStartPosition) {
player.seekTo(startWindow, startPosition);
}
player.setMediaSources(mediaSources, /* resetPosition= */ !haveStartPosition);
player.prepare();
updateButtonVisibility();
}
private List<MediaSource> createTopLevelMediaSources(Intent intent) {
String action = intent.getAction();
boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
if (!actionIsListView && !ACTION_VIEW.equals(action)) {
showToast(getString(R.string.unexpected_intent_action, action));
finish();
return Collections.emptyList();
}
Sample intentAsSample = Sample.createFromIntent(intent);
UriSample[] samples =
intentAsSample instanceof Sample.PlaylistSample
? ((Sample.PlaylistSample) intentAsSample).children
: new UriSample[] {(UriSample) intentAsSample};
boolean seenAdsTagUri = false;
for (UriSample sample : samples) {
seenAdsTagUri |= sample.adTagUri != null;
if (!Util.checkCleartextTrafficPermitted(sample.uri)) {
showToast(R.string.error_cleartext_not_permitted);
return Collections.emptyList();
}
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) {
// The player will be reinitialized if the permission is granted.
return Collections.emptyList();
}
}
List<MediaSource> mediaSources = new ArrayList<>();
for (UriSample sample : samples) {
MediaSource mediaSource = createLeafMediaSource(sample);
if (mediaSource == null) {
continue;
}
Sample.SubtitleInfo subtitleInfo = sample.subtitleInfo;
if (subtitleInfo != null) {
if (Util.maybeRequestReadExternalStoragePermission(
/* activity= */ this, subtitleInfo.uri)) {
// The player will be reinitialized if the permission is granted.
return Collections.emptyList();
}
Format subtitleFormat =
Format.createTextSampleFormat(
/* id= */ null,
subtitleInfo.mimeType,
C.SELECTION_FLAG_DEFAULT,
subtitleInfo.language);
MediaSource subtitleMediaSource =
new SingleSampleMediaSource.Factory(dataSourceFactory)
.createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET);
mediaSource = new MergingMediaSource(mediaSource, subtitleMediaSource);
}
mediaSources.add(mediaSource);
}
if (seenAdsTagUri && mediaSources.size() == 1) {
Uri adTagUri = samples[0].adTagUri;
if (!adTagUri.equals(loadedAdTagUri)) {
releaseAdsLoader();
loadedAdTagUri = adTagUri;
}
MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri);
if (adsMediaSource != null) {
mediaSources.set(0, adsMediaSource);
} else {
showToast(R.string.ima_not_loaded);
}
} else if (seenAdsTagUri && mediaSources.size() > 1) {
showToast(R.string.unsupported_ads_in_concatenation);
releaseAdsLoader();
} else {
releaseAdsLoader();
}
return mediaSources;
}
@Nullable
private MediaSource createLeafMediaSource(UriSample parameters) {
Sample.DrmInfo drmInfo = parameters.drmInfo;
int errorStringId = R.string.error_drm_unknown;
DrmSessionManager<ExoMediaCrypto> drmSessionManager = null;
if (drmInfo == null) {
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
} else if (Util.SDK_INT < 18) {
errorStringId = R.string.error_drm_unsupported_before_api_18;
} else if (!MediaDrm.isCryptoSchemeSupported(drmInfo.drmScheme)) {
errorStringId = R.string.error_drm_unsupported_scheme;
} else {
MediaDrmCallback mediaDrmCallback =
createMediaDrmCallback(drmInfo.drmLicenseUrl, drmInfo.drmKeyRequestProperties);
drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(drmInfo.drmScheme, FrameworkMediaDrm.DEFAULT_PROVIDER)
.setMultiSession(drmInfo.drmMultiSession)
.setUseDrmSessionsForClearContent(drmInfo.drmSessionForClearTypes)
.build(mediaDrmCallback);
}
if (drmSessionManager == null) {
showToast(errorStringId);
finish();
return null;
}
DownloadRequest downloadRequest =
((DemoApplication) getApplication())
.getDownloadTracker()
.getDownloadRequest(parameters.uri);
if (downloadRequest != null) {
return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
}
return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager);
}
private MediaSource createLeafMediaSource(
Uri uri, String extension, DrmSessionManager<?> drmSessionManager) {
@ContentType int type = Util.inferContentType(uri, extension);
switch (type) {
case C.TYPE_DASH:
return new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
case C.TYPE_SS:
return new SsMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
case C.TYPE_OTHER:
return new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
}
private HttpMediaDrmCallback createMediaDrmCallback(
String licenseUrl, String[] keyRequestPropertiesArray) {
HttpDataSource.Factory licenseDataSourceFactory =
((DemoApplication) getApplication()).buildHttpDataSourceFactory();
HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
if (keyRequestPropertiesArray != null) {
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
keyRequestPropertiesArray[i + 1]);
}
}
return drmCallback;
}
private void releasePlayer() {
if (player != null) {
updateTrackSelectorParameters();
updateStartPosition();
debugViewHelper.stop();
debugViewHelper = null;
player.release();
player = null;
mediaSources = null;
trackSelector = null;
}
if (adsLoader != null) {
adsLoader.setPlayer(null);
}
}
private void releaseAdsLoader() {
if (adsLoader != null) {
adsLoader.release();
adsLoader = null;
loadedAdTagUri = null;
playerView.getOverlayFrameLayout().removeAllViews();
}
}
private void updateTrackSelectorParameters() {
if (trackSelector != null) {
trackSelectorParameters = trackSelector.getParameters();
}
}
private void updateStartPosition() {
if (player != null) {
startAutoPlay = player.getPlayWhenReady();
startWindow = player.getCurrentWindowIndex();
startPosition = Math.max(0, player.getContentPosition());
}
}
private void clearStartPosition() {
startAutoPlay = true;
startWindow = C.INDEX_UNSET;
startPosition = C.TIME_UNSET;
}
/** Returns a new DataSource factory. */
private DataSource.Factory buildDataSourceFactory() {
return ((DemoApplication) getApplication()).buildDataSourceFactory();
}
/** Returns an ads media source, reusing the ads loader if one exists. */
@Nullable
private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
// Load the extension source using reflection so the demo app doesn't have to depend on it.
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
try {
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
if (adsLoader == null) {
// Full class names used so the lint rule triggers should any of the classes move.
// LINT.IfChange
Constructor<? extends AdsLoader> loaderConstructor =
loaderClass
.asSubclass(AdsLoader.class)
.getConstructor(android.content.Context.class, android.net.Uri.class);
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
adsLoader = loaderConstructor.newInstance(this, adTagUri);
}
MediaSourceFactory adMediaSourceFactory =
new MediaSourceFactory() {
private DrmSessionManager<?> drmSessionManager =
DrmSessionManager.getDummyDrmSessionManager();
@Override
public MediaSourceFactory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {
this.drmSessionManager = drmSessionManager;
return this;
}
@Override
public MediaSource createMediaSource(Uri uri) {
return PlayerActivity.this.createLeafMediaSource(
uri, /* extension=*/ null, drmSessionManager);
}
@Override
public int[] getSupportedTypes() {
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
}
};
return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView);
} catch (ClassNotFoundException e) {
// IMA extension not loaded.
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// User controls
private void updateButtonVisibility() {
selectTracksButton.setEnabled(
player != null && TrackSelectionDialog.willHaveContent(trackSelector));
}
private void showControls() {
debugRootView.setVisibility(View.VISIBLE);
}
private void showToast(int messageId) {
showToast(getString(messageId));
}
private void showToast(String message) {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
}
private static boolean isBehindLiveWindow(ExoPlaybackException e) {
if (e.type != ExoPlaybackException.TYPE_SOURCE) {
return false;
}
Throwable cause = e.getSourceException();
while (cause != null) {
if (cause instanceof BehindLiveWindowException) {
return true;
}
cause = cause.getCause();
}
return false;
}
private class PlayerEventListener implements Player.EventListener {
@Override
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playbackState == Player.STATE_ENDED) {
showControls();
}
updateButtonVisibility();
}
@Override
public void onPlayerError(ExoPlaybackException e) {
if (isBehindLiveWindow(e)) {
clearStartPosition();
initializePlayer();
} else {
updateButtonVisibility();
showControls();
}
}
@Override
@SuppressWarnings("ReferenceEquality")
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
updateButtonVisibility();
if (trackGroups != lastSeenTrackGroupArray) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_video);
}
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_audio);
}
}
lastSeenTrackGroupArray = trackGroups;
}
}
}
private class PlayerErrorMessageProvider implements ErrorMessageProvider<ExoPlaybackException> {
@Override
public Pair<Integer, String> getErrorMessage(ExoPlaybackException e) {
String errorString = getString(R.string.error_generic);
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
Exception cause = e.getRendererException();
if (cause instanceof DecoderInitializationException) {
// Special case for decoder initialization failures.
DecoderInitializationException decoderInitializationException =
(DecoderInitializationException) cause;
if (decoderInitializationException.codecInfo == null) {
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
errorString = getString(R.string.error_querying_decoders);
} else if (decoderInitializationException.secureDecoderRequired) {
errorString =
getString(
R.string.error_no_secure_decoder, decoderInitializationException.mimeType);
} else {
errorString =
getString(R.string.error_no_decoder, decoderInitializationException.mimeType);
}
} else {
errorString =
getString(
R.string.error_instantiating_decoder,
decoderInitializationException.codecInfo.name);
}
}
}
return Pair.create(0, errorString);
}
}
}

View File

@ -0,0 +1,277 @@
/*
* Copyright (C) 2019 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.exoplayer2.demo;
import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST;
import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SESSION_FOR_CLEAR_TYPES_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.IS_LIVE_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_LANGUAGE_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_MIME_TYPE_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_URI_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.UUID;
/* package */ abstract class Sample {
public static final class UriSample extends Sample {
public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) {
String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix);
String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix);
boolean isLive =
intent.getBooleanExtra(IS_LIVE_EXTRA + extrasKeySuffix, /* defaultValue= */ false);
Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null;
return new UriSample(
/* name= */ null,
uri,
extension,
isLive,
DrmInfo.createFromIntent(intent, extrasKeySuffix),
adTagUri,
/* sphericalStereoMode= */ null,
SubtitleInfo.createFromIntent(intent, extrasKeySuffix));
}
public final Uri uri;
public final String extension;
public final boolean isLive;
public final DrmInfo drmInfo;
public final Uri adTagUri;
@Nullable public final String sphericalStereoMode;
@Nullable SubtitleInfo subtitleInfo;
public UriSample(
String name,
Uri uri,
String extension,
boolean isLive,
DrmInfo drmInfo,
Uri adTagUri,
@Nullable String sphericalStereoMode,
@Nullable SubtitleInfo subtitleInfo) {
super(name);
this.uri = uri;
this.extension = extension;
this.isLive = isLive;
this.drmInfo = drmInfo;
this.adTagUri = adTagUri;
this.sphericalStereoMode = sphericalStereoMode;
this.subtitleInfo = subtitleInfo;
}
@Override
public void addToIntent(Intent intent) {
intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri);
intent.putExtra(PlayerActivity.IS_LIVE_EXTRA, isLive);
intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ "");
}
public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) {
intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString());
intent.putExtra(PlayerActivity.IS_LIVE_EXTRA + extrasKeySuffix, isLive);
addPlayerConfigToIntent(intent, extrasKeySuffix);
}
private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) {
intent
.putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension)
.putExtra(
AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null);
if (drmInfo != null) {
drmInfo.addToIntent(intent, extrasKeySuffix);
}
if (subtitleInfo != null) {
subtitleInfo.addToIntent(intent, extrasKeySuffix);
}
}
}
public static final class PlaylistSample extends Sample {
public final UriSample[] children;
public PlaylistSample(String name, UriSample... children) {
super(name);
this.children = children;
}
@Override
public void addToIntent(Intent intent) {
intent.setAction(PlayerActivity.ACTION_VIEW_LIST);
for (int i = 0; i < children.length; i++) {
children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i);
}
}
}
public static final class DrmInfo {
public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) {
String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix;
if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) {
return null;
}
String drmSchemeExtra =
intent.hasExtra(schemeKey)
? intent.getStringExtra(schemeKey)
: intent.getStringExtra(schemeUuidKey);
UUID drmScheme = Util.getDrmUuid(drmSchemeExtra);
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix);
String[] keyRequestPropertiesArray =
intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
String[] drmSessionForClearTypesExtra =
intent.getStringArrayExtra(DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix);
int[] drmSessionForClearTypes = toTrackTypeArray(drmSessionForClearTypesExtra);
boolean drmMultiSession =
intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false);
return new DrmInfo(
drmScheme,
drmLicenseUrl,
keyRequestPropertiesArray,
drmSessionForClearTypes,
drmMultiSession);
}
public final UUID drmScheme;
public final String drmLicenseUrl;
public final String[] drmKeyRequestProperties;
public final int[] drmSessionForClearTypes;
public final boolean drmMultiSession;
public DrmInfo(
UUID drmScheme,
String drmLicenseUrl,
String[] drmKeyRequestProperties,
int[] drmSessionForClearTypes,
boolean drmMultiSession) {
this.drmScheme = drmScheme;
this.drmLicenseUrl = drmLicenseUrl;
this.drmKeyRequestProperties = drmKeyRequestProperties;
this.drmSessionForClearTypes = drmSessionForClearTypes;
this.drmMultiSession = drmMultiSession;
}
public void addToIntent(Intent intent, String extrasKeySuffix) {
Assertions.checkNotNull(intent);
intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString());
intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl);
intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
ArrayList<String> typeStrings = new ArrayList<>();
for (int type : drmSessionForClearTypes) {
// Only audio and video are supported.
typeStrings.add(type == C.TRACK_TYPE_AUDIO ? "audio" : "video");
}
intent.putExtra(
DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix, typeStrings.toArray(new String[0]));
intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession);
}
}
public static final class SubtitleInfo {
@Nullable
public static SubtitleInfo createFromIntent(Intent intent, String extrasKeySuffix) {
if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) {
return null;
}
return new SubtitleInfo(
Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)),
intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix),
intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix));
}
public final Uri uri;
public final String mimeType;
@Nullable public final String language;
public SubtitleInfo(Uri uri, String mimeType, @Nullable String language) {
this.uri = Assertions.checkNotNull(uri);
this.mimeType = Assertions.checkNotNull(mimeType);
this.language = language;
}
public void addToIntent(Intent intent, String extrasKeySuffix) {
intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, uri.toString());
intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, mimeType);
intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, language);
}
}
public static int[] toTrackTypeArray(@Nullable String[] trackTypeStringsArray) {
if (trackTypeStringsArray == null) {
return new int[0];
}
HashSet<Integer> trackTypes = new HashSet<>();
for (String trackTypeString : trackTypeStringsArray) {
switch (Util.toLowerInvariant(trackTypeString)) {
case "audio":
trackTypes.add(C.TRACK_TYPE_AUDIO);
break;
case "video":
trackTypes.add(C.TRACK_TYPE_VIDEO);
break;
default:
throw new IllegalArgumentException("Invalid track type: " + trackTypeString);
}
}
return Util.toArray(new ArrayList<>(trackTypes));
}
public static Sample createFromIntent(Intent intent) {
if (ACTION_VIEW_LIST.equals(intent.getAction())) {
ArrayList<String> intentUris = new ArrayList<>();
int index = 0;
while (intent.hasExtra(URI_EXTRA + "_" + index)) {
intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index));
index++;
}
UriSample[] children = new UriSample[intentUris.size()];
for (int i = 0; i < children.length; i++) {
Uri uri = Uri.parse(intentUris.get(i));
children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i);
}
return new PlaylistSample(/* name= */ null, children);
} else {
return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ "");
}
}
@Nullable public final String name;
public Sample(String name) {
this.name = name;
}
public abstract void addToIntent(Intent intent);
}

View File

@ -0,0 +1,553 @@
/*
* Copyright (C) 2016 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.exoplayer2.demo;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.JsonReader;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.demo.Sample.DrmInfo;
import com.google.android.exoplayer2.demo.Sample.PlaylistSample;
import com.google.android.exoplayer2.demo.Sample.UriSample;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** An activity for selecting from a list of media samples. */
public class SampleChooserActivity extends AppCompatActivity
implements DownloadTracker.Listener, OnChildClickListener {
private static final String TAG = "SampleChooserActivity";
private boolean useExtensionRenderers;
private DownloadTracker downloadTracker;
private SampleAdapter sampleAdapter;
private MenuItem preferExtensionDecodersMenuItem;
private MenuItem randomAbrMenuItem;
private MenuItem tunnelingMenuItem;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sample_chooser_activity);
sampleAdapter = new SampleAdapter();
ExpandableListView sampleListView = findViewById(R.id.sample_list);
sampleListView.setAdapter(sampleAdapter);
sampleListView.setOnChildClickListener(this);
Intent intent = getIntent();
String dataUri = intent.getDataString();
String[] uris;
if (dataUri != null) {
uris = new String[] {dataUri};
} else {
ArrayList<String> uriList = new ArrayList<>();
AssetManager assetManager = getAssets();
try {
for (String asset : assetManager.list("")) {
if (asset.endsWith(".exolist.json")) {
uriList.add("asset:///" + asset);
}
}
} catch (IOException e) {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
.show();
}
uris = new String[uriList.size()];
uriList.toArray(uris);
Arrays.sort(uris);
}
DemoApplication application = (DemoApplication) getApplication();
useExtensionRenderers = application.useExtensionRenderers();
downloadTracker = application.getDownloadTracker();
SampleListLoader loaderTask = new SampleListLoader();
loaderTask.execute(uris);
// Start the download service if it should be running but it's not currently.
// Starting the service in the foreground causes notification flicker if there is no scheduled
// action. Starting it in the background throws an exception if the app is in the background too
// (e.g. if device screen is locked).
try {
DownloadService.start(this, DemoDownloadService.class);
} catch (IllegalStateException e) {
DownloadService.startForeground(this, DemoDownloadService.class);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.sample_chooser_menu, menu);
preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders);
preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers);
randomAbrMenuItem = menu.findItem(R.id.random_abr);
tunnelingMenuItem = menu.findItem(R.id.tunneling);
if (Util.SDK_INT < 21) {
tunnelingMenuItem.setEnabled(false);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
item.setChecked(!item.isChecked());
return true;
}
@Override
public void onStart() {
super.onStart();
downloadTracker.addListener(this);
sampleAdapter.notifyDataSetChanged();
}
@Override
public void onStop() {
downloadTracker.removeListener(this);
super.onStop();
}
@Override
public void onDownloadsChanged() {
sampleAdapter.notifyDataSetChanged();
}
private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) {
if (sawError) {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
.show();
}
sampleAdapter.setSampleGroups(groups);
}
@Override
public boolean onChildClick(
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
Sample sample = (Sample) view.getTag();
Intent intent = new Intent(this, PlayerActivity.class);
intent.putExtra(
PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA,
isNonNullAndChecked(preferExtensionDecodersMenuItem));
String abrAlgorithm =
isNonNullAndChecked(randomAbrMenuItem)
? PlayerActivity.ABR_ALGORITHM_RANDOM
: PlayerActivity.ABR_ALGORITHM_DEFAULT;
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
intent.putExtra(PlayerActivity.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem));
sample.addToIntent(intent);
startActivity(intent);
return true;
}
private void onSampleDownloadButtonClicked(Sample sample) {
int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample);
if (downloadUnsupportedStringId != 0) {
Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
.show();
} else {
UriSample uriSample = (UriSample) sample;
RenderersFactory renderersFactory =
((DemoApplication) getApplication())
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
downloadTracker.toggleDownload(
getSupportFragmentManager(),
sample.name,
uriSample.uri,
uriSample.extension,
renderersFactory);
}
}
private int getDownloadUnsupportedStringId(Sample sample) {
if (sample instanceof PlaylistSample) {
return R.string.download_playlist_unsupported;
}
UriSample uriSample = (UriSample) sample;
if (uriSample.drmInfo != null) {
return R.string.download_drm_unsupported;
}
if (uriSample.isLive) {
return R.string.download_live_unsupported;
}
if (uriSample.adTagUri != null) {
return R.string.download_ads_unsupported;
}
String scheme = uriSample.uri.getScheme();
if (!("http".equals(scheme) || "https".equals(scheme))) {
return R.string.download_scheme_unsupported;
}
return 0;
}
private static boolean isNonNullAndChecked(@Nullable MenuItem menuItem) {
// Temporary workaround for layouts that do not inflate the options menu.
return menuItem != null && menuItem.isChecked();
}
private final class SampleListLoader extends AsyncTask<String, Void, List<SampleGroup>> {
private boolean sawError;
@Override
protected List<SampleGroup> doInBackground(String... uris) {
List<SampleGroup> result = new ArrayList<>();
Context context = getApplicationContext();
String userAgent = Util.getUserAgent(context, "ExoPlayerDemo");
DataSource dataSource =
new DefaultDataSource(context, userAgent, /* allowCrossProtocolRedirects= */ false);
for (String uri : uris) {
DataSpec dataSpec = new DataSpec(Uri.parse(uri));
InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
try {
readSampleGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
} catch (Exception e) {
Log.e(TAG, "Error loading sample list: " + uri, e);
sawError = true;
} finally {
Util.closeQuietly(dataSource);
}
}
return result;
}
@Override
protected void onPostExecute(List<SampleGroup> result) {
onSampleGroups(result, sawError);
}
private void readSampleGroups(JsonReader reader, List<SampleGroup> groups) throws IOException {
reader.beginArray();
while (reader.hasNext()) {
readSampleGroup(reader, groups);
}
reader.endArray();
}
private void readSampleGroup(JsonReader reader, List<SampleGroup> groups) throws IOException {
String groupName = "";
ArrayList<Sample> samples = new ArrayList<>();
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
switch (name) {
case "name":
groupName = reader.nextString();
break;
case "samples":
reader.beginArray();
while (reader.hasNext()) {
samples.add(readEntry(reader, false));
}
reader.endArray();
break;
case "_comment":
reader.nextString(); // Ignore.
break;
default:
throw new ParserException("Unsupported name: " + name);
}
}
reader.endObject();
SampleGroup group = getGroup(groupName, groups);
group.samples.addAll(samples);
}
private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
String sampleName = null;
Uri uri = null;
String extension = null;
boolean isLive = false;
String drmScheme = null;
String drmLicenseUrl = null;
String[] drmKeyRequestProperties = null;
String[] drmSessionForClearTypes = null;
boolean drmMultiSession = false;
ArrayList<UriSample> playlistSamples = null;
String adTagUri = null;
String sphericalStereoMode = null;
List<Sample.SubtitleInfo> subtitleInfos = new ArrayList<>();
Uri subtitleUri = null;
String subtitleMimeType = null;
String subtitleLanguage = null;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
switch (name) {
case "name":
sampleName = reader.nextString();
break;
case "uri":
uri = Uri.parse(reader.nextString());
break;
case "extension":
extension = reader.nextString();
break;
case "drm_scheme":
drmScheme = reader.nextString();
break;
case "is_live":
isLive = reader.nextBoolean();
break;
case "drm_license_url":
drmLicenseUrl = reader.nextString();
break;
case "drm_key_request_properties":
ArrayList<String> drmKeyRequestPropertiesList = new ArrayList<>();
reader.beginObject();
while (reader.hasNext()) {
drmKeyRequestPropertiesList.add(reader.nextName());
drmKeyRequestPropertiesList.add(reader.nextString());
}
reader.endObject();
drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]);
break;
case "drm_session_for_clear_types":
ArrayList<String> drmSessionForClearTypesList = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
drmSessionForClearTypesList.add(reader.nextString());
}
reader.endArray();
drmSessionForClearTypes = drmSessionForClearTypesList.toArray(new String[0]);
break;
case "drm_multi_session":
drmMultiSession = reader.nextBoolean();
break;
case "playlist":
Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
playlistSamples = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
playlistSamples.add((UriSample) readEntry(reader, /* insidePlaylist= */ true));
}
reader.endArray();
break;
case "ad_tag_uri":
adTagUri = reader.nextString();
break;
case "spherical_stereo_mode":
Assertions.checkState(
!insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode");
sphericalStereoMode = reader.nextString();
break;
case "subtitle_uri":
subtitleUri = Uri.parse(reader.nextString());
break;
case "subtitle_mime_type":
subtitleMimeType = reader.nextString();
break;
case "subtitle_language":
subtitleLanguage = reader.nextString();
break;
default:
throw new ParserException("Unsupported attribute name: " + name);
}
}
reader.endObject();
DrmInfo drmInfo =
drmScheme == null
? null
: new DrmInfo(
Util.getDrmUuid(drmScheme),
drmLicenseUrl,
drmKeyRequestProperties,
Sample.toTrackTypeArray(drmSessionForClearTypes),
drmMultiSession);
Sample.SubtitleInfo subtitleInfo =
subtitleUri == null
? null
: new Sample.SubtitleInfo(
subtitleUri,
Assertions.checkNotNull(
subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."),
subtitleLanguage);
if (playlistSamples != null) {
UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
return new PlaylistSample(sampleName, playlistSamplesArray);
} else {
return new UriSample(
sampleName,
uri,
extension,
isLive,
drmInfo,
adTagUri != null ? Uri.parse(adTagUri) : null,
sphericalStereoMode,
subtitleInfo);
}
}
private SampleGroup getGroup(String groupName, List<SampleGroup> groups) {
for (int i = 0; i < groups.size(); i++) {
if (Util.areEqual(groupName, groups.get(i).title)) {
return groups.get(i);
}
}
SampleGroup group = new SampleGroup(groupName);
groups.add(group);
return group;
}
}
private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {
private List<SampleGroup> sampleGroups;
public SampleAdapter() {
sampleGroups = Collections.emptyList();
}
public void setSampleGroups(List<SampleGroup> sampleGroups) {
this.sampleGroups = sampleGroups;
notifyDataSetChanged();
}
@Override
public Sample getChild(int groupPosition, int childPosition) {
return getGroup(groupPosition).samples.get(childPosition);
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false);
View downloadButton = view.findViewById(R.id.download_button);
downloadButton.setOnClickListener(this);
downloadButton.setFocusable(false);
}
initializeChildView(view, getChild(groupPosition, childPosition));
return view;
}
@Override
public int getChildrenCount(int groupPosition) {
return getGroup(groupPosition).samples.size();
}
@Override
public SampleGroup getGroup(int groupPosition) {
return sampleGroups.get(groupPosition);
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
ViewGroup parent) {
View view = convertView;
if (view == null) {
view =
getLayoutInflater()
.inflate(android.R.layout.simple_expandable_list_item_1, parent, false);
}
((TextView) view).setText(getGroup(groupPosition).title);
return view;
}
@Override
public int getGroupCount() {
return sampleGroups.size();
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
@Override
public void onClick(View view) {
onSampleDownloadButtonClicked((Sample) view.getTag());
}
private void initializeChildView(View view, Sample sample) {
view.setTag(sample);
TextView sampleTitle = view.findViewById(R.id.sample_title);
sampleTitle.setText(sample.name);
boolean canDownload = getDownloadUnsupportedStringId(sample) == 0;
boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri);
ImageButton downloadButton = view.findViewById(R.id.download_button);
downloadButton.setTag(sample);
downloadButton.setColorFilter(
canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666);
downloadButton.setImageResource(
isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download);
}
}
private static final class SampleGroup {
public final String title;
public final List<Sample> samples;
public SampleGroup(String title) {
this.title = title;
this.samples = new ArrayList<>();
}
}
}

View File

@ -0,0 +1,368 @@
/*
* Copyright (C) 2019 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.exoplayer2.demo;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.ui.TrackSelectionView;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.material.tabs.TabLayout;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Dialog to select tracks. */
public final class TrackSelectionDialog extends DialogFragment {
private final SparseArray<TrackSelectionViewFragment> tabFragments;
private final ArrayList<Integer> tabTrackTypes;
private int titleId;
private DialogInterface.OnClickListener onClickListener;
private DialogInterface.OnDismissListener onDismissListener;
/**
* Returns whether a track selection dialog will have content to display if initialized with the
* specified {@link DefaultTrackSelector} in its current state.
*/
public static boolean willHaveContent(DefaultTrackSelector trackSelector) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
return mappedTrackInfo != null && willHaveContent(mappedTrackInfo);
}
/**
* Returns whether a track selection dialog will have content to display if initialized with the
* specified {@link MappedTrackInfo}.
*/
public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) {
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
if (showTabForRenderer(mappedTrackInfo, i)) {
return true;
}
}
return false;
}
/**
* Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be
* automatically updated when tracks are selected.
*
* @param trackSelector The {@link DefaultTrackSelector}.
* @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is
* dismissed.
*/
public static TrackSelectionDialog createForTrackSelector(
DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) {
MappedTrackInfo mappedTrackInfo =
Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());
TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog();
DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();
trackSelectionDialog.init(
/* titleId= */ R.string.track_selection_title,
mappedTrackInfo,
/* initialParameters = */ parameters,
/* allowAdaptiveSelections =*/ true,
/* allowMultipleOverrides= */ false,
/* onClickListener= */ (dialog, which) -> {
DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon();
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
builder
.clearSelectionOverrides(/* rendererIndex= */ i)
.setRendererDisabled(
/* rendererIndex= */ i,
trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i));
List<SelectionOverride> overrides =
trackSelectionDialog.getOverrides(/* rendererIndex= */ i);
if (!overrides.isEmpty()) {
builder.setSelectionOverride(
/* rendererIndex= */ i,
mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i),
overrides.get(0));
}
}
trackSelector.setParameters(builder);
},
onDismissListener);
return trackSelectionDialog;
}
/**
* Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}.
*
* @param titleId The resource id of the dialog title.
* @param mappedTrackInfo The {@link MappedTrackInfo} to display.
* @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the initial
* track selection.
* @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track)
* can be made.
* @param allowMultipleOverrides Whether tracks from multiple track groups can be selected.
* @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are selected.
* @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is
* dismissed.
*/
public static TrackSelectionDialog createForMappedTrackInfoAndParameters(
int titleId,
MappedTrackInfo mappedTrackInfo,
DefaultTrackSelector.Parameters initialParameters,
boolean allowAdaptiveSelections,
boolean allowMultipleOverrides,
DialogInterface.OnClickListener onClickListener,
DialogInterface.OnDismissListener onDismissListener) {
TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog();
trackSelectionDialog.init(
titleId,
mappedTrackInfo,
initialParameters,
allowAdaptiveSelections,
allowMultipleOverrides,
onClickListener,
onDismissListener);
return trackSelectionDialog;
}
public TrackSelectionDialog() {
tabFragments = new SparseArray<>();
tabTrackTypes = new ArrayList<>();
// Retain instance across activity re-creation to prevent losing access to init data.
setRetainInstance(true);
}
private void init(
int titleId,
MappedTrackInfo mappedTrackInfo,
DefaultTrackSelector.Parameters initialParameters,
boolean allowAdaptiveSelections,
boolean allowMultipleOverrides,
DialogInterface.OnClickListener onClickListener,
DialogInterface.OnDismissListener onDismissListener) {
this.titleId = titleId;
this.onClickListener = onClickListener;
this.onDismissListener = onDismissListener;
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
if (showTabForRenderer(mappedTrackInfo, i)) {
int trackType = mappedTrackInfo.getRendererType(/* rendererIndex= */ i);
TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i);
TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment();
tabFragment.init(
mappedTrackInfo,
/* rendererIndex= */ i,
initialParameters.getRendererDisabled(/* rendererIndex= */ i),
initialParameters.getSelectionOverride(/* rendererIndex= */ i, trackGroupArray),
allowAdaptiveSelections,
allowMultipleOverrides);
tabFragments.put(i, tabFragment);
tabTrackTypes.add(trackType);
}
}
}
/**
* Returns whether a renderer is disabled.
*
* @param rendererIndex Renderer index.
* @return Whether the renderer is disabled.
*/
public boolean getIsDisabled(int rendererIndex) {
TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex);
return rendererView != null && rendererView.isDisabled;
}
/**
* Returns the list of selected track selection overrides for the specified renderer. There will
* be at most one override for each track group.
*
* @param rendererIndex Renderer index.
* @return The list of track selection overrides for this renderer.
*/
public List<SelectionOverride> getOverrides(int rendererIndex) {
TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex);
return rendererView == null ? Collections.emptyList() : rendererView.overrides;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// We need to own the view to let tab layout work correctly on all API levels. We can't use
// AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using
// the AlertDialog theme overlay with force-enabled title.
AppCompatDialog dialog =
new AppCompatDialog(getActivity(), R.style.TrackSelectionDialogThemeOverlay);
dialog.setTitle(titleId);
return dialog;
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
onDismissListener.onDismiss(dialog);
}
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false);
TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout);
ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager);
Button cancelButton = dialogView.findViewById(R.id.track_selection_dialog_cancel_button);
Button okButton = dialogView.findViewById(R.id.track_selection_dialog_ok_button);
viewPager.setAdapter(new FragmentAdapter(getChildFragmentManager()));
tabLayout.setupWithViewPager(viewPager);
tabLayout.setVisibility(tabFragments.size() > 1 ? View.VISIBLE : View.GONE);
cancelButton.setOnClickListener(view -> dismiss());
okButton.setOnClickListener(
view -> {
onClickListener.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
dismiss();
});
return dialogView;
}
private static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) {
TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex);
if (trackGroupArray.length == 0) {
return false;
}
int trackType = mappedTrackInfo.getRendererType(rendererIndex);
return isSupportedTrackType(trackType);
}
private static boolean isSupportedTrackType(int trackType) {
switch (trackType) {
case C.TRACK_TYPE_VIDEO:
case C.TRACK_TYPE_AUDIO:
case C.TRACK_TYPE_TEXT:
return true;
default:
return false;
}
}
private static String getTrackTypeString(Resources resources, int trackType) {
switch (trackType) {
case C.TRACK_TYPE_VIDEO:
return resources.getString(R.string.exo_track_selection_title_video);
case C.TRACK_TYPE_AUDIO:
return resources.getString(R.string.exo_track_selection_title_audio);
case C.TRACK_TYPE_TEXT:
return resources.getString(R.string.exo_track_selection_title_text);
default:
throw new IllegalArgumentException();
}
}
private final class FragmentAdapter extends FragmentPagerAdapter {
public FragmentAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
@Override
public Fragment getItem(int position) {
return tabFragments.valueAt(position);
}
@Override
public int getCount() {
return tabFragments.size();
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return getTrackTypeString(getResources(), tabTrackTypes.get(position));
}
}
/** Fragment to show a track selection in tab of the track selection dialog. */
public static final class TrackSelectionViewFragment extends Fragment
implements TrackSelectionView.TrackSelectionListener {
private MappedTrackInfo mappedTrackInfo;
private int rendererIndex;
private boolean allowAdaptiveSelections;
private boolean allowMultipleOverrides;
/* package */ boolean isDisabled;
/* package */ List<SelectionOverride> overrides;
public TrackSelectionViewFragment() {
// Retain instance across activity re-creation to prevent losing access to init data.
setRetainInstance(true);
}
public void init(
MappedTrackInfo mappedTrackInfo,
int rendererIndex,
boolean initialIsDisabled,
@Nullable SelectionOverride initialOverride,
boolean allowAdaptiveSelections,
boolean allowMultipleOverrides) {
this.mappedTrackInfo = mappedTrackInfo;
this.rendererIndex = rendererIndex;
this.isDisabled = initialIsDisabled;
this.overrides =
initialOverride == null
? Collections.emptyList()
: Collections.singletonList(initialOverride);
this.allowAdaptiveSelections = allowAdaptiveSelections;
this.allowMultipleOverrides = allowMultipleOverrides;
}
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View rootView =
inflater.inflate(
R.layout.exo_track_selection_dialog, container, /* attachToRoot= */ false);
TrackSelectionView trackSelectionView = rootView.findViewById(R.id.exo_track_selection_view);
trackSelectionView.setShowDisableOption(true);
trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides);
trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);
trackSelectionView.init(
mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this);
return rootView;
}
@Override
public void onTrackSelectionChanged(boolean isDisabled, List<SelectionOverride> overrides) {
this.isDisabled = isDisabled;
this.overrides = overrides;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

View File

@ -20,7 +20,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:keepScreenOn="true"> android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view" <com.google.android.exoplayer2.ui.PlayerView android:id="@+id/player_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
@ -44,11 +44,11 @@
android:orientation="horizontal" android:orientation="horizontal"
android:visibility="gone"> android:visibility="gone">
<Button android:id="@+id/retry_button" <Button android:id="@+id/select_tracks_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/retry" android:text="@string/track_selection_title"
android:visibility="gone"/> android:enabled="false"/>
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView android:id="@+id/sample_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
<ImageButton android:id="@+id/download_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/exo_download_description"
android:background="@android:color/transparent"/>
</LinearLayout>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager.widget.ViewPager
android:id="@+id/track_selection_dialog_view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.google.android.material.tabs.TabLayout
android:id="@+id/track_selection_dialog_tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="fill"
app:tabMode="fixed"/>
</androidx.viewpager.widget.ViewPager>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end">
<Button
android:id="@+id/track_selection_dialog_cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel"
style="?android:attr/borderlessButtonStyle"/>
<Button
android:id="@+id/track_selection_dialog_ok_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok"
style="?android:attr/borderlessButtonStyle"/>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/prefer_extension_decoders"
android:title="@string/prefer_extension_decoders"
android:checkable="true"
app:showAsAction="never"/>
<item android:id="@+id/random_abr"
android:title="@string/random_abr"
android:checkable="true"
app:showAsAction="never"/>
<item android:id="@+id/tunneling"
android:title="@string/tunneling"
android:checkable="true"
app:showAsAction="never"/>
</menu>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -13,28 +13,23 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="application_name">ExoPlayer</string> <string name="application_name">ExoPlayer</string>
<string name="video">Video</string> <string name="track_selection_title">Select tracks</string>
<string name="audio">Audio</string>
<string name="text">Text</string>
<string name="retry">Retry</string>
<string name="selection_disabled">Disabled</string>
<string name="selection_default">Default</string>
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string> <string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
<string name="enable_random_adaptation">Enable random adaptation</string> <string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string> <string name="error_generic">Playback failed</string>
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string>
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
<string name="error_drm_unsupported_before_api_18">Protected content not supported on API levels below 18</string>
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string> <string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
@ -56,4 +51,26 @@
<string name="sample_list_load_error">One or more sample lists failed to load</string> <string name="sample_list_load_error">One or more sample lists failed to load</string>
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
<string name="unsupported_ads_in_concatenation">Playing sample without ads, as ads are not supported in concatenations</string>
<string name="download_start_error">Failed to start download</string>
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
<string name="download_drm_unsupported">This demo app does not support downloading protected content</string>
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
<string name="download_live_unsupported">This demo app does not support downloading live content</string>
<string name="download_ads_unsupported">IMA does not support offline ads</string>
<string name="prefer_extension_decoders">Prefer extension decoders</string>
<string name="random_abr">Enable random ABR</string>
<string name="tunneling">Request multimedia tunneling</string>
</resources> </resources>

View File

@ -13,12 +13,18 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="PlayerTheme" parent="android:Theme.Holo"> <style name="TrackSelectionDialogThemeOverlay" parent="ThemeOverlay.AppCompat.Dialog.Alert">
<item name="android:windowNoTitle">true</item> <item name="windowNoTitle">false</item>
</style>
<style name="PlayerTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@android:color/black</item>
</style> </style>
<style name="PlayerTheme.Spherical">
<item name="surface_type">spherical_gl_surface_view</item>
</style>
</resources> </resources>

21
demos/surface/README.md Normal file
View File

@ -0,0 +1,21 @@
# ExoPlayer SurfaceControl demo
This app demonstrates how to use the [SurfaceControl][] API to redirect video
output from ExoPlayer between different views or off-screen. `SurfaceControl`
is new in Android 10, so the app requires `minSdkVersion` 29.
The app layout has a grid of `SurfaceViews`. Initially video is output to one
of the views. Tap a `SurfaceView` to move video output to it. You can also tap
the buttons at the top of the activity to move video output off-screen, to a
full-screen `SurfaceView` or to a new activity.
When using `SurfaceControl`, the `MediaCodec` always has the same surface
attached to it, which can be freely 'reparented' to any `SurfaceView` (or
off-screen) without any interruptions to playback. This works better than
calling `MediaCodec.setOutputSurface` to change the output surface of the codec
because `MediaCodec` does not re-render its last frame when that method is
called, and because you can move output off-screen easily (`setOutputSurface`
can't take a `null` surface, so the player has to use a `DummySurface`, which
doesn't handle protected output on all devices).
[SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl

View File

@ -1,4 +1,4 @@
// Copyright (C) 2016 The Android Open Source Project // Copyright (C) 2019 The Android Open Source Project
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -11,15 +11,22 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
minSdkVersion 16 versionName project.ext.releaseVersion
targetSdkVersion project.ext.targetSdkVersion versionCode project.ext.releaseVersionCode
minSdkVersion 29
targetSdkVersion project.ext.appTargetSdkVersion
} }
buildTypes { buildTypes {
@ -28,30 +35,17 @@ android {
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt') proguardFiles getDefaultProguardFile('proguard-android.txt')
} }
debug {
jniDebuggable = true
}
} }
lintOptions { lintOptions {
// The demo app does not have translations. // This demo app does not have translations.
disable 'MissingTranslation' disable 'MissingTranslation'
} }
productFlavors {
noExtensions
withExtensions
}
} }
dependencies { dependencies {
compile project(':library-core') implementation project(modulePrefix + 'library-core')
compile project(':library-dash') implementation project(modulePrefix + 'library-ui')
compile project(':library-hls') implementation project(modulePrefix + 'library-dash')
compile project(':library-smoothstreaming') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compile project(':library-ui')
withExtensionsCompile project(path: ':extension-ffmpeg')
withExtensionsCompile project(path: ':extension-flac')
withExtensionsCompile project(path: ':extension-opus')
withExtensionsCompile project(path: ':extension-vp9')
} }

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2019 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.exoplayer2.surfacedemo">
<uses-sdk/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="com.google.android.exoplayer.surfacedemo.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="content"/>
<data android:scheme="asset"/>
<data android:scheme="file"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,282 @@
/*
* Copyright (C) 2019 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.exoplayer2.surfacedemo;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.GridLayout;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.UUID;
/** Activity that demonstrates use of {@link SurfaceControl} with ExoPlayer. */
public final class MainActivity extends Activity {
private static final String DEFAULT_MEDIA_URI =
"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv";
private static final String SURFACE_CONTROL_NAME = "surfacedemo";
private static final String ACTION_VIEW = "com.google.android.exoplayer.surfacedemo.action.VIEW";
private static final String EXTENSION_EXTRA = "extension";
private static final String DRM_SCHEME_EXTRA = "drm_scheme";
private static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
private static final String OWNER_EXTRA = "owner";
private boolean isOwner;
@Nullable private PlayerControlView playerControlView;
@Nullable private SurfaceView fullScreenView;
@Nullable private SurfaceView nonFullScreenView;
@Nullable private SurfaceView currentOutputView;
@Nullable private static SimpleExoPlayer player;
@Nullable private static SurfaceControl surfaceControl;
@Nullable private static Surface videoSurface;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
playerControlView = findViewById(R.id.player_control_view);
fullScreenView = findViewById(R.id.full_screen_view);
fullScreenView.setOnClickListener(
v -> {
setCurrentOutputView(nonFullScreenView);
Assertions.checkNotNull(fullScreenView).setVisibility(View.GONE);
});
attachSurfaceListener(fullScreenView);
isOwner = getIntent().getBooleanExtra(OWNER_EXTRA, /* defaultValue= */ true);
GridLayout gridLayout = findViewById(R.id.grid_layout);
for (int i = 0; i < 9; i++) {
View view;
if (i == 0) {
Button button = new Button(/* context= */ this);
view = button;
button.setText(getString(R.string.no_output_label));
button.setOnClickListener(v -> reparent(/* surfaceView= */ null));
} else if (i == 1) {
Button button = new Button(/* context= */ this);
view = button;
button.setText(getString(R.string.full_screen_label));
button.setOnClickListener(
v -> {
setCurrentOutputView(fullScreenView);
Assertions.checkNotNull(fullScreenView).setVisibility(View.VISIBLE);
});
} else if (i == 2) {
Button button = new Button(/* context= */ this);
view = button;
button.setText(getString(R.string.new_activity_label));
button.setOnClickListener(
v ->
startActivity(
new Intent(MainActivity.this, MainActivity.class)
.putExtra(OWNER_EXTRA, /* value= */ false)));
} else {
SurfaceView surfaceView = new SurfaceView(this);
view = surfaceView;
attachSurfaceListener(surfaceView);
surfaceView.setOnClickListener(
v -> {
setCurrentOutputView(surfaceView);
nonFullScreenView = surfaceView;
});
if (nonFullScreenView == null) {
nonFullScreenView = surfaceView;
}
}
gridLayout.addView(view);
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams();
layoutParams.width = 0;
layoutParams.height = 0;
layoutParams.columnSpec = GridLayout.spec(i % 3, 1f);
layoutParams.rowSpec = GridLayout.spec(i / 3, 1f);
layoutParams.bottomMargin = 10;
layoutParams.leftMargin = 10;
layoutParams.topMargin = 10;
layoutParams.rightMargin = 10;
view.setLayoutParams(layoutParams);
}
}
@Override
public void onResume() {
super.onResume();
if (isOwner && player == null) {
initializePlayer();
}
setCurrentOutputView(nonFullScreenView);
PlayerControlView playerControlView = Assertions.checkNotNull(this.playerControlView);
playerControlView.setPlayer(player);
playerControlView.show();
}
@Override
public void onPause() {
super.onPause();
Assertions.checkNotNull(playerControlView).setPlayer(null);
}
@Override
public void onDestroy() {
super.onDestroy();
if (isOwner && isFinishing()) {
if (surfaceControl != null) {
surfaceControl.release();
surfaceControl = null;
}
if (videoSurface != null) {
videoSurface.release();
videoSurface = null;
}
if (player != null) {
player.release();
player = null;
}
}
}
private void initializePlayer() {
Intent intent = getIntent();
String action = intent.getAction();
Uri uri =
ACTION_VIEW.equals(action)
? Assertions.checkNotNull(intent.getData())
: Uri.parse(DEFAULT_MEDIA_URI);
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
DrmSessionManager<ExoMediaCrypto> drmSessionManager;
if (intent.hasExtra(DRM_SCHEME_EXTRA)) {
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback);
} else {
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
}
DataSource.Factory dataSourceFactory =
new DefaultDataSourceFactory(
this, Util.getUserAgent(this, getString(R.string.application_name)));
MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
if (type == C.TYPE_DASH) {
mediaSource =
new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
} else if (type == C.TYPE_OTHER) {
mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
} else {
throw new IllegalStateException();
}
SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build();
player.prepare(mediaSource);
player.play();
player.setRepeatMode(Player.REPEAT_MODE_ALL);
surfaceControl =
new SurfaceControl.Builder()
.setName(SURFACE_CONTROL_NAME)
.setBufferSize(/* width= */ 0, /* height= */ 0)
.build();
videoSurface = new Surface(surfaceControl);
player.setVideoSurface(videoSurface);
MainActivity.player = player;
}
private void setCurrentOutputView(@Nullable SurfaceView surfaceView) {
currentOutputView = surfaceView;
if (surfaceView != null && surfaceView.getHolder().getSurface() != null) {
reparent(surfaceView);
}
}
private void attachSurfaceListener(SurfaceView surfaceView) {
surfaceView
.getHolder()
.addCallback(
new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
if (surfaceView == currentOutputView) {
reparent(surfaceView);
}
}
@Override
public void surfaceChanged(
SurfaceHolder surfaceHolder, int format, int width, int height) {}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {}
});
}
private static void reparent(@Nullable SurfaceView surfaceView) {
SurfaceControl surfaceControl = Assertions.checkNotNull(MainActivity.surfaceControl);
if (surfaceView == null) {
new SurfaceControl.Transaction()
.reparent(surfaceControl, /* newParent= */ null)
.setBufferSize(surfaceControl, /* w= */ 0, /* h= */ 0)
.setVisibility(surfaceControl, /* visible= */ false)
.apply();
} else {
SurfaceControl newParentSurfaceControl = surfaceView.getSurfaceControl();
new SurfaceControl.Transaction()
.reparent(surfaceControl, newParentSurfaceControl)
.setBufferSize(surfaceControl, surfaceView.getWidth(), surfaceView.getHeight())
.setVisibility(surfaceControl, /* visible= */ true)
.apply();
}
}
}

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2019 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<GridLayout
android:id="@+id/grid_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:columnCount="3"/>
<com.google.android.exoplayer2.ui.PlayerControlView
android:id="@+id/player_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:show_timeout="0"/>
</LinearLayout>
<SurfaceView
android:id="@+id/full_screen_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
</FrameLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,6 +1,5 @@
<?xml version="1.0"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!-- Copyright (C) 2019 The Android Open Source Project
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,11 +14,10 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<string name="exo_controls_previous_description">"Eelmine lugu"</string>
<string name="exo_controls_next_description">"Järgmine lugu"</string> <string name="application_name">ExoPlayer SurfaceControl demo</string>
<string name="exo_controls_pause_description">"Peata"</string> <string name="no_output_label">No output</string>
<string name="exo_controls_play_description">"Esita"</string> <string name="full_screen_label">Full screen</string>
<string name="exo_controls_stop_description">"Peata"</string> <string name="new_activity_label">New activity</string>
<string name="exo_controls_rewind_description">"Keri tagasi"</string>
<string name="exo_controls_fastforward_description">"Keri edasi"</string>
</resources> </resources>

5
extensions/README.md Normal file
View File

@ -0,0 +1,5 @@
# ExoPlayer extensions #
ExoPlayer extensions are modules that depend on external libraries to provide
additional functionality. Browse the individual extensions and their READMEs to
learn more.

134
extensions/av1/README.md Normal file
View File

@ -0,0 +1,134 @@
# ExoPlayer AV1 extension #
The AV1 extension provides `Libgav1VideoRenderer`, which uses libgav1 native
library to decode AV1 videos.
## License note ##
Please note that whilst the code in this repository is licensed under
[Apache 2.0][], using this extension also requires building and including one or
more external libraries as described below. These are licensed separately.
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
## Build instructions (Linux, macOS) ##
To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
In addition, it's necessary to fetch cpu_features library and libgav1 with its
dependencies as follows:
* Set the following environment variables:
```
cd "<path to exoplayer checkout>"
EXOPLAYER_ROOT="$(pwd)"
AV1_EXT_PATH="${EXOPLAYER_ROOT}/extensions/av1/src/main"
```
* Fetch cpu_features library:
```
cd "${AV1_EXT_PATH}/jni" && \
git clone https://github.com/google/cpu_features
```
* Fetch libgav1:
```
cd "${AV1_EXT_PATH}/jni" && \
git clone https://chromium.googlesource.com/codecs/libgav1 libgav1
```
* Fetch Abseil:
```
cd "${AV1_EXT_PATH}/jni/libgav1" && \
git clone https://github.com/abseil/abseil-cpp.git third_party/abseil-cpp
```
* [Install CMake][].
Having followed these steps, gradle will build the extension automatically when
run on the command line or via Android Studio, using [CMake][] and [Ninja][]
to configure and build libgav1 and the extension's [JNI wrapper library][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Install CMake]: https://developer.android.com/studio/projects/install-ndk
[CMake]: https://cmake.org/
[Ninja]: https://ninja-build.org
[JNI wrapper library]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/av1/src/main/jni/gav1_jni.cc
## Build instructions (Windows) ##
We do not provide support for building this extension on Windows, however it
should be possible to follow the Linux instructions in [Windows PowerShell][].
[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
## Using the extension ##
Once you've followed the instructions above to check out, build and depend on
the extension, the next step is to tell ExoPlayer to use `Libgav1VideoRenderer`.
How you do this depends on which player API you're using:
* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
you can enable using the extension by setting the `extensionRendererMode`
parameter of the `DefaultRenderersFactory` constructor to
`EXTENSION_RENDERER_MODE_ON`. This will use `Libgav1VideoRenderer` for
playback if `MediaCodecVideoRenderer` doesn't support decoding the input AV1
stream. Pass `EXTENSION_RENDERER_MODE_PREFER` to give `Libgav1VideoRenderer`
priority over `MediaCodecVideoRenderer`.
* If you've subclassed `DefaultRenderersFactory`, add a `Libvgav1VideoRenderer`
to the output list in `buildVideoRenderers`. ExoPlayer will use the first
`Renderer` in the list that supports the input media format.
* If you've implemented your own `RenderersFactory`, return a
`Libgav1VideoRenderer` instance from `createRenderers`. ExoPlayer will use the
first `Renderer` in the returned array that supports the input media format.
* If you're using `ExoPlayer.Builder`, pass a `Libgav1VideoRenderer` in the
array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that
supports the input media format.
Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation.
You need to make sure you are passing a `Libgav1VideoRenderer` to the player and
then you need to implement your own logic to use the renderer for a given track.
## Using the extension in the demo application ##
To try out playback using the extension in the [demo application][], see
[enabling extension decoders][].
[demo application]: https://exoplayer.dev/demo-application.html
[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders
## Rendering options ##
There are two possibilities for rendering the output `Libgav1VideoRenderer`
gets from the libgav1 decoder:
* GL rendering using GL shader for color space conversion
* If you are using `SimpleExoPlayer` with `PlayerView`, enable this option by
setting `surface_type` of `PlayerView` to be
`video_decoder_gl_surface_view`.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message
of type `C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER` with an instance of
`VideoDecoderOutputBufferRenderer` as its object.
* Native rendering using `ANativeWindow`
* If you are using `SimpleExoPlayer` with `PlayerView`, this option is enabled
by default.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message of
type `C.MSG_SET_SURFACE` with an instance of `SurfaceView` as its object.
Note: Although the default option uses `ANativeWindow`, based on our testing the
GL rendering mode has better performance, so should be preferred
## Links ##
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.av1.*`
belong to this module.
[Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -0,0 +1,73 @@
// Copyright (C) 2019 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 from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
externalNativeBuild {
cmake {
// Debug CMake build type causes video frames to drop,
// so native library should always use Release build type.
arguments "-DCMAKE_BUILD_TYPE=Release"
targets "gav1JNI"
}
}
}
// This option resolves the problem of finding libgav1JNI.so
// on multiple paths. The first one found is picked.
packagingOptions {
pickFirst 'lib/arm64-v8a/libgav1JNI.so'
pickFirst 'lib/armeabi-v7a/libgav1JNI.so'
pickFirst 'lib/x86/libgav1JNI.so'
pickFirst 'lib/x86_64/libgav1JNI.so'
}
sourceSets.main {
// As native JNI library build is invoked from gradle, this is
// not needed. However, it exposes the built library and keeps
// consistency with the other extensions.
jniLibs.srcDir 'src/main/libs'
}
}
// Configure the native build only if libgav1 is present, to avoid gradle sync
// failures if libgav1 hasn't been checked out according to the README and CMake
// isn't installed.
if (project.file('src/main/jni/libgav1').exists()) {
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
android.externalNativeBuild.cmake.version = '3.7.1+'
}
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
}
ext {
javadocTitle = 'AV1 extension'
}
apply from: '../../javadoc_library.gradle'

View File

@ -0,0 +1,7 @@
# Proguard rules specific to the AV1 extension.
# This prevents the names of native methods from being obfuscated.
-keepclasseswithmembernames class * {
native <methods>;
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2019 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 package="com.google.android.exoplayer2.ext.av1"/>

View File

@ -0,0 +1,234 @@
/*
* Copyright (C) 2019 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.exoplayer2.ext.av1;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
import java.nio.ByteBuffer;
/** Gav1 decoder. */
/* package */ final class Gav1Decoder
extends SimpleDecoder<VideoDecoderInputBuffer, VideoDecoderOutputBuffer, Gav1DecoderException> {
// LINT.IfChange
private static final int GAV1_ERROR = 0;
private static final int GAV1_OK = 1;
private static final int GAV1_DECODE_ONLY = 2;
// LINT.ThenChange(../../../../../../../jni/gav1_jni.cc)
private final long gav1DecoderContext;
@C.VideoOutputMode private volatile int outputMode;
/**
* Creates a Gav1Decoder.
*
* @param numInputBuffers Number of input buffers.
* @param numOutputBuffers Number of output buffers.
* @param initialInputBufferSize The initial size of each input buffer, in bytes.
* @param threads Number of threads libgav1 will use to decode.
* @throws Gav1DecoderException Thrown if an exception occurs when initializing the decoder.
*/
public Gav1Decoder(
int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, int threads)
throws Gav1DecoderException {
super(
new VideoDecoderInputBuffer[numInputBuffers],
new VideoDecoderOutputBuffer[numOutputBuffers]);
if (!Gav1Library.isAvailable()) {
throw new Gav1DecoderException("Failed to load decoder native library.");
}
gav1DecoderContext = gav1Init(threads);
if (gav1DecoderContext == GAV1_ERROR || gav1CheckError(gav1DecoderContext) == GAV1_ERROR) {
throw new Gav1DecoderException(
"Failed to initialize decoder. Error: " + gav1GetErrorMessage(gav1DecoderContext));
}
setInitialInputBufferSize(initialInputBufferSize);
}
@Override
public String getName() {
return "libgav1";
}
/**
* Sets the output mode for frames rendered by the decoder.
*
* @param outputMode The output mode.
*/
public void setOutputMode(@C.VideoOutputMode int outputMode) {
this.outputMode = outputMode;
}
@Override
protected VideoDecoderInputBuffer createInputBuffer() {
return new VideoDecoderInputBuffer();
}
@Override
protected VideoDecoderOutputBuffer createOutputBuffer() {
return new VideoDecoderOutputBuffer(this::releaseOutputBuffer);
}
@Nullable
@Override
protected Gav1DecoderException decode(
VideoDecoderInputBuffer inputBuffer, VideoDecoderOutputBuffer outputBuffer, boolean reset) {
ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
int inputSize = inputData.limit();
if (gav1Decode(gav1DecoderContext, inputData, inputSize) == GAV1_ERROR) {
return new Gav1DecoderException(
"gav1Decode error: " + gav1GetErrorMessage(gav1DecoderContext));
}
boolean decodeOnly = inputBuffer.isDecodeOnly();
if (!decodeOnly) {
outputBuffer.init(inputBuffer.timeUs, outputMode, /* supplementalData= */ null);
}
// We need to dequeue the decoded frame from the decoder even when the input data is
// decode-only.
int getFrameResult = gav1GetFrame(gav1DecoderContext, outputBuffer, decodeOnly);
if (getFrameResult == GAV1_ERROR) {
return new Gav1DecoderException(
"gav1GetFrame error: " + gav1GetErrorMessage(gav1DecoderContext));
}
if (getFrameResult == GAV1_DECODE_ONLY) {
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
}
if (!decodeOnly) {
outputBuffer.colorInfo = inputBuffer.colorInfo;
}
return null;
}
@Override
protected Gav1DecoderException createUnexpectedDecodeException(Throwable error) {
return new Gav1DecoderException("Unexpected decode error", error);
}
@Override
public void release() {
super.release();
gav1Close(gav1DecoderContext);
}
@Override
protected void releaseOutputBuffer(VideoDecoderOutputBuffer buffer) {
// Decode only frames do not acquire a reference on the internal decoder buffer and thus do not
// require a call to gav1ReleaseFrame.
if (buffer.mode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
gav1ReleaseFrame(gav1DecoderContext, buffer);
}
super.releaseOutputBuffer(buffer);
}
/**
* Renders output buffer to the given surface. Must only be called when in {@link
* C#VIDEO_OUTPUT_MODE_SURFACE_YUV} mode.
*
* @param outputBuffer Output buffer.
* @param surface Output surface.
* @throws Gav1DecoderException Thrown if called with invalid output mode or frame rendering
* fails.
*/
public void renderToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
throws Gav1DecoderException {
if (outputBuffer.mode != C.VIDEO_OUTPUT_MODE_SURFACE_YUV) {
throw new Gav1DecoderException("Invalid output mode.");
}
if (gav1RenderFrame(gav1DecoderContext, surface, outputBuffer) == GAV1_ERROR) {
throw new Gav1DecoderException(
"Buffer render error: " + gav1GetErrorMessage(gav1DecoderContext));
}
}
/**
* Initializes a libgav1 decoder.
*
* @param threads Number of threads to be used by a libgav1 decoder.
* @return The address of the decoder context or {@link #GAV1_ERROR} if there was an error.
*/
private native long gav1Init(int threads);
/**
* Deallocates the decoder context.
*
* @param context Decoder context.
*/
private native void gav1Close(long context);
/**
* Decodes the encoded data passed.
*
* @param context Decoder context.
* @param encodedData Encoded data.
* @param length Length of the data buffer.
* @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occurred.
*/
private native int gav1Decode(long context, ByteBuffer encodedData, int length);
/**
* Gets the decoded frame.
*
* @param context Decoder context.
* @param outputBuffer Output buffer for the decoded frame.
* @return {@link #GAV1_OK} if successful, {@link #GAV1_DECODE_ONLY} if successful but the frame
* is decode-only, {@link #GAV1_ERROR} if an error occurred.
*/
private native int gav1GetFrame(
long context, VideoDecoderOutputBuffer outputBuffer, boolean decodeOnly);
/**
* Renders the frame to the surface. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only.
*
* @param context Decoder context.
* @param surface Output surface.
* @param outputBuffer Output buffer with the decoded frame.
* @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occurred.
*/
private native int gav1RenderFrame(
long context, Surface surface, VideoDecoderOutputBuffer outputBuffer);
/**
* Releases the frame. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only.
*
* @param context Decoder context.
* @param outputBuffer Output buffer.
*/
private native void gav1ReleaseFrame(long context, VideoDecoderOutputBuffer outputBuffer);
/**
* Returns a human-readable string describing the last error encountered in the given context.
*
* @param context Decoder context.
* @return A string describing the last encountered error.
*/
private native String gav1GetErrorMessage(long context);
/**
* Returns whether an error occurred.
*
* @param context Decoder context.
* @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occurred.
*/
private native int gav1CheckError(long context);
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2019 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.exoplayer2.ext.av1;
import com.google.android.exoplayer2.video.VideoDecoderException;
/** Thrown when a libgav1 decoder error occurs. */
public final class Gav1DecoderException extends VideoDecoderException {
/* package */ Gav1DecoderException(String message) {
super(message);
}
/* package */ Gav1DecoderException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2019 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.exoplayer2.ext.av1;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
/** Configures and queries the underlying native library. */
public final class Gav1Library {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.gav1");
}
private static final LibraryLoader LOADER = new LibraryLoader("gav1JNI");
private Gav1Library() {}
/** Returns whether the underlying library is available, loading it if necessary. */
public static boolean isAvailable() {
return LOADER.isAvailable();
}
}

View File

@ -0,0 +1,197 @@
/*
* Copyright (C) 2019 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.exoplayer2.ext.av1;
import static java.lang.Runtime.getRuntime;
import android.os.Handler;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer;
import com.google.android.exoplayer2.video.VideoDecoderException;
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
/**
* Decodes and renders video using libgav1 decoder.
*
* <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
* on the playback thread:
*
* <ul>
* <li>Message with type {@link #MSG_SET_SURFACE} to set the output surface. The message payload
* should be the target {@link Surface}, or null.
* <li>Message with type {@link #MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER} to set the output
* buffer renderer. The message payload should be the target {@link
* VideoDecoderOutputBufferRenderer}, or null.
* </ul>
*/
public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4;
private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4;
/* Default size based on 720p resolution video compressed by a factor of two. */
private static final int DEFAULT_INPUT_BUFFER_SIZE =
Util.ceilDivide(1280, 64) * Util.ceilDivide(720, 64) * (64 * 64 * 3 / 2) / 2;
/** The number of input buffers. */
private final int numInputBuffers;
/**
* The number of output buffers. The renderer may limit the minimum possible value due to
* requiring multiple output buffers to be dequeued at a time for it to make progress.
*/
private final int numOutputBuffers;
private final int threads;
@Nullable private Gav1Decoder decoder;
/**
* Creates a Libgav1VideoRenderer.
*
* @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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
*/
public Libgav1VideoRenderer(
long allowedJoiningTimeMs,
@Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
this(
allowedJoiningTimeMs,
eventHandler,
eventListener,
maxDroppedFramesToNotify,
/* threads= */ getRuntime().availableProcessors(),
DEFAULT_NUM_OF_INPUT_BUFFERS,
DEFAULT_NUM_OF_OUTPUT_BUFFERS);
}
/**
* Creates a Libgav1VideoRenderer.
*
* @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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
* @param threads Number of threads libgav1 will use to decode.
* @param numInputBuffers Number of input buffers.
* @param numOutputBuffers Number of output buffers.
*/
public Libgav1VideoRenderer(
long allowedJoiningTimeMs,
@Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify,
int threads,
int numInputBuffers,
int numOutputBuffers) {
super(
allowedJoiningTimeMs,
eventHandler,
eventListener,
maxDroppedFramesToNotify,
/* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false);
this.threads = threads;
this.numInputBuffers = numInputBuffers;
this.numOutputBuffers = numOutputBuffers;
}
@Override
@Capabilities
protected int supportsFormatInternal(
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType)
|| !Gav1Library.isAvailable()) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
}
if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
}
return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
}
@Override
protected SimpleDecoder<
VideoDecoderInputBuffer,
? extends VideoDecoderOutputBuffer,
? extends VideoDecoderException>
createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws VideoDecoderException {
TraceUtil.beginSection("createGav1Decoder");
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
Gav1Decoder decoder =
new Gav1Decoder(numInputBuffers, numOutputBuffers, initialInputBufferSize, threads);
this.decoder = decoder;
TraceUtil.endSection();
return decoder;
}
@Override
protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
throws Gav1DecoderException {
if (decoder == null) {
throw new Gav1DecoderException(
"Failed to render output buffer to surface: decoder is not initialized.");
}
decoder.renderToSurface(outputBuffer, surface);
outputBuffer.release();
}
@Override
protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) {
if (decoder != null) {
decoder.setOutputMode(outputMode);
}
}
// PlayerMessage.Target implementation.
@Override
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
if (messageType == MSG_SET_SURFACE) {
setOutputSurface((Surface) message);
} else if (messageType == MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) {
setOutputBufferRenderer((VideoDecoderOutputBufferRenderer) message);
} else {
super.handleMessage(messageType, message);
}
}
}

Some files were not shown because too many files have changed in this diff Show More