Merge branch 'dev-v2' into subtitle_color_in_cue_class
# Conflicts: # library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java
57
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal 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 you’re able to share as source code on GitHub.
|
||||
|
||||
### [REQUIRED] Link to test content
|
||||
Provide a JSON snippet for the demo app’s 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.
|
||||
30
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
|
|
@ -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. It’s 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 you’ve already looked for an answer to your question. It’s
|
||||
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 app’s 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.
|
||||
11
.gitignore
vendored
|
|
@ -37,6 +37,12 @@ local.properties
|
|||
proguard.cfg
|
||||
proguard-project.txt
|
||||
|
||||
# Bazel
|
||||
bazel-bin
|
||||
bazel-genfiles
|
||||
bazel-out
|
||||
bazel-testlogs
|
||||
|
||||
# Other
|
||||
.DS_Store
|
||||
cmake-build-debug
|
||||
|
|
@ -51,6 +57,10 @@ 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/cpu_features
|
||||
extensions/av1/src/main/jni/libgav1
|
||||
|
||||
# Opus extension
|
||||
extensions/opus/src/main/jni/libopus
|
||||
|
||||
|
|
@ -65,4 +75,3 @@ extensions/cronet/jniLibs/*
|
|||
!extensions/cronet/jniLibs/README.md
|
||||
extensions/cronet/libs/*
|
||||
!extensions/cronet/libs/README.md
|
||||
|
||||
|
|
|
|||
12
.hgignore
|
|
@ -12,13 +12,14 @@ libs
|
|||
obj
|
||||
lint.xml
|
||||
|
||||
# IntelliJ IDEA
|
||||
# IntelliJ IDEA & Android Studio
|
||||
.idea
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
classes
|
||||
gen-external-apklibs
|
||||
*.li
|
||||
|
||||
# Eclipse
|
||||
.project
|
||||
|
|
@ -44,6 +45,12 @@ local.properties
|
|||
proguard.cfg
|
||||
proguard-project.txt
|
||||
|
||||
# Bazel
|
||||
bazel-bin
|
||||
bazel-genfiles
|
||||
bazel-out
|
||||
bazel-testlogs
|
||||
|
||||
# Other
|
||||
.DS_Store
|
||||
cmake-build-debug
|
||||
|
|
@ -55,6 +62,9 @@ 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
|
||||
|
||||
|
|
|
|||
495
.idea/codeStyleSettings.xml
Normal 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>
|
||||
|
|
@ -16,9 +16,8 @@ all of the information requested in the issue template.
|
|||
## Pull requests ##
|
||||
|
||||
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
|
||||
be suitable for merging into older `dev-vX` branches. Before a pull request can
|
||||
be accepted you must submit a Contributor License Agreement, as described below.
|
||||
into the `dev-v2` branch. Before a pull request can be accepted you must submit
|
||||
a Contributor License Agreement, as described below.
|
||||
|
||||
[dev]: https://github.com/google/ExoPlayer/tree/dev
|
||||
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
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 using a subject in the format "Issue #1234".
|
||||
|
||||
### 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 using a subject in the format
|
||||
"Issue #1234".
|
||||
|
||||
35
README.md
|
|
@ -15,8 +15,8 @@ and extend, and can be updated through Play Store application updates.
|
|||
* Follow our [developer blog][] to keep up to date with the latest ExoPlayer
|
||||
developments!
|
||||
|
||||
[developer guide]: https://google.github.io/ExoPlayer/guide.html
|
||||
[class reference]: https://google.github.io/ExoPlayer/doc/reference
|
||||
[developer guide]: https://exoplayer.dev/guide.html
|
||||
[class reference]: https://exoplayer.dev/doc/reference
|
||||
[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
||||
[developer blog]: https://medium.com/google-exoplayer
|
||||
|
||||
|
|
@ -27,17 +27,21 @@ 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
|
||||
dependency. You need to make sure you have the JCenter and Google repositories
|
||||
dependency. You need to make sure you have the Google and JCenter repositories
|
||||
included in the `build.gradle` file in the root of your project:
|
||||
|
||||
```gradle
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Add ExoPlayer module dependencies ####
|
||||
|
||||
Next add a dependency in the `build.gradle` file of your app module. The
|
||||
following will add a dependency to the full library:
|
||||
|
||||
|
|
@ -45,10 +49,12 @@ following will add a dependency to the full library:
|
|||
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
||||
```
|
||||
|
||||
where `2.X.X` is your preferred version. Alternatively, you can depend on only
|
||||
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
|
||||
an app that plays DASH content:
|
||||
where `2.X.X` is your preferred version.
|
||||
|
||||
As an alternative to the full library, you can depend on only 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 an app that
|
||||
plays DASH content:
|
||||
|
||||
```gradle
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
|
||||
|
|
@ -77,6 +83,18 @@ JCenter can be found on [Bintray][].
|
|||
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
|
||||
[Bintray]: https://bintray.com/google/exoplayer
|
||||
|
||||
#### 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
|
||||
|
|
@ -89,6 +107,7 @@ branch:
|
|||
|
||||
```sh
|
||||
git clone https://github.com/google/ExoPlayer.git
|
||||
cd ExoPlayer
|
||||
git checkout release-v2
|
||||
```
|
||||
|
||||
|
|
|
|||
1259
RELEASENOTES.md
20
build.gradle
|
|
@ -13,29 +13,22 @@
|
|||
// limitations under the License.
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.0.1'
|
||||
classpath 'com.novoda:bintray-release:0.5.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'
|
||||
}
|
||||
classpath 'com.android.tools.build:gradle:3.5.1'
|
||||
classpath 'com.novoda:bintray-release:0.9.1'
|
||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0'
|
||||
}
|
||||
}
|
||||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
project.ext {
|
||||
exoplayerPublishEnabled = true
|
||||
exoplayerPublishEnabled = false
|
||||
}
|
||||
if (it.hasProperty('externalBuildDir')) {
|
||||
if (!new File(externalBuildDir).isAbsolute()) {
|
||||
|
|
@ -43,6 +36,7 @@ allprojects {
|
|||
}
|
||||
buildDir = "${externalBuildDir}/${project.name}"
|
||||
}
|
||||
group = 'com.google.android.exoplayer'
|
||||
}
|
||||
|
||||
apply from: 'javadoc_combined.gradle'
|
||||
|
|
|
|||
|
|
@ -13,24 +13,29 @@
|
|||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.7.1'
|
||||
releaseVersionCode = 2701
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
||||
// 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 = 14
|
||||
targetSdkVersion = 27
|
||||
compileSdkVersion = 27
|
||||
buildToolsVersion = '26.0.2'
|
||||
testSupportLibraryVersion = '0.5'
|
||||
supportLibraryVersion = '27.0.0'
|
||||
playServicesLibraryVersion = '11.4.2'
|
||||
dexmakerVersion = '1.2'
|
||||
mockitoVersion = '1.9.5'
|
||||
junitVersion = '4.12'
|
||||
truthVersion = '0.39'
|
||||
robolectricVersion = '3.7.1'
|
||||
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
|
||||
|
|
|
|||
|
|
@ -18,18 +18,22 @@ if (gradle.ext.has('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 + 'testutils-robolectric'
|
||||
include modulePrefix + 'testdata'
|
||||
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'
|
||||
|
|
@ -37,20 +41,25 @@ 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 + 'testutils-robolectric').projectDir = new File(rootDir, 'testutils_robolectric')
|
||||
project(modulePrefix + 'testdata').projectDir = new File(rootDir, 'testdata')
|
||||
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')
|
||||
|
|
@ -58,9 +67,4 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio
|
|||
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')
|
||||
|
||||
if (gradle.ext.has('exoplayerIncludeCronetExtension')
|
||||
&& gradle.ext.exoplayerIncludeCronetExtension) {
|
||||
include modulePrefix + 'extension-cronet'
|
||||
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
|
||||
}
|
||||
project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager')
|
||||
|
|
|
|||
|
|
@ -16,20 +16,27 @@ apply plugin: 'com.android.application'
|
|||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion 16
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
proguardFiles = [
|
||||
"proguard-rules.txt",
|
||||
getDefaultProguardFile('proguard-android.txt')
|
||||
]
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
|
|
@ -37,10 +44,9 @@ android {
|
|||
}
|
||||
|
||||
lintOptions {
|
||||
// The demo app does not have translations.
|
||||
disable 'MissingTranslation'
|
||||
// The demo app isn't indexed and doesn't have translations.
|
||||
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -50,7 +56,9 @@ dependencies {
|
|||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'extension-cast')
|
||||
implementation 'com.android.support:support-v4:' + supportLibraryVersion
|
||||
implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
||||
implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion
|
||||
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'
|
||||
|
|
|
|||
6
demos/cast/proguard-rules.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Proguard rules specific to the Cast demo app.
|
||||
|
||||
# Accessed via menu.xml
|
||||
-keep class androidx.mediarouter.app.MediaRouteActionProvider {
|
||||
*;
|
||||
}
|
||||
|
|
@ -17,13 +17,15 @@
|
|||
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" />
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -15,15 +15,16 @@
|
|||
*/
|
||||
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 com.google.android.gms.cast.MediaInfo;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility methods and constants for the Cast demo application.
|
||||
*/
|
||||
/** Utility methods and constants for the Cast demo application. */
|
||||
/* package */ final class DemoUtil {
|
||||
|
||||
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
|
||||
|
|
@ -31,62 +32,73 @@ import java.util.List;
|
|||
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<Sample> SAMPLES;
|
||||
|
||||
/**
|
||||
* Represents a media sample.
|
||||
*/
|
||||
public static final class Sample {
|
||||
|
||||
/**
|
||||
* The uri from which the media sample is obtained.
|
||||
*/
|
||||
public final String uri;
|
||||
/**
|
||||
* A descriptive name for the sample.
|
||||
*/
|
||||
public final String name;
|
||||
/**
|
||||
* The mime type of the media sample, as required by {@link MediaInfo#setContentType}.
|
||||
*/
|
||||
public final String mimeType;
|
||||
|
||||
/**
|
||||
* @param uri See {@link #uri}.
|
||||
* @param name See {@link #name}.
|
||||
* @param mimeType See {@link #mimeType}.
|
||||
*/
|
||||
public Sample(String uri, String name, String mimeType) {
|
||||
this.uri = uri;
|
||||
this.name = name;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
/** The list of samples available in the cast demo app. */
|
||||
public static final List<MediaItem> SAMPLES;
|
||||
|
||||
static {
|
||||
// App samples.
|
||||
ArrayList<Sample> samples = new ArrayList<>();
|
||||
samples.add(new Sample("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
||||
"DASH (clear,MP4,H264)", MIME_TYPE_DASH));
|
||||
samples.add(new Sample("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
|
||||
+ "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS));
|
||||
samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)",
|
||||
MIME_TYPE_VIDEO_MP4));
|
||||
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() {}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,38 +17,40 @@ package com.google.android.exoplayer2.castdemo;
|
|||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.graphics.ColorUtils;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
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.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
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.castdemo.DemoUtil.Sample;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
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 {@link CastPlayer}.
|
||||
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
|
||||
* Cast extension.
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity implements OnClickListener,
|
||||
PlayerManager.QueuePositionListener {
|
||||
public class MainActivity extends AppCompatActivity
|
||||
implements OnClickListener, PlayerManager.Listener {
|
||||
|
||||
private PlayerView localPlayerView;
|
||||
private PlayerControlView castControlView;
|
||||
|
|
@ -63,7 +65,20 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Getting the cast context later than onStart can cause device discovery not to take place.
|
||||
castContext = CastContext.getSharedInstance(this);
|
||||
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);
|
||||
|
||||
|
|
@ -93,9 +108,13 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (castContext == null) {
|
||||
// There is no Cast context to work with. Do nothing.
|
||||
return;
|
||||
}
|
||||
playerManager =
|
||||
PlayerManager.createPlayerManager(
|
||||
/* queuePositionListener= */ this,
|
||||
new PlayerManager(
|
||||
/* listener= */ this,
|
||||
localPlayerView,
|
||||
castControlView,
|
||||
/* context= */ this,
|
||||
|
|
@ -106,9 +125,14 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||
@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.
|
||||
|
|
@ -121,12 +145,15 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title)
|
||||
.setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create()
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.add_samples)
|
||||
.setView(buildSampleListView())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
// PlayerManager.QueuePositionListener implementation.
|
||||
// PlayerManager.Listener implementation.
|
||||
|
||||
@Override
|
||||
public void onQueuePositionChanged(int previousIndex, int newIndex) {
|
||||
|
|
@ -138,43 +165,37 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
new OnItemClickListener() {
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
playerManager.addItem(DemoUtil.SAMPLES.get(position));
|
||||
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
|
||||
}
|
||||
(parent, view, position, id) -> {
|
||||
playerManager.addItem(DemoUtil.SAMPLES.get(position));
|
||||
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
|
||||
});
|
||||
return dialogList;
|
||||
}
|
||||
|
||||
// Internal classes.
|
||||
|
||||
private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
|
||||
|
||||
public final TextView textView;
|
||||
|
||||
public QueueItemViewHolder(TextView textView) {
|
||||
super(textView);
|
||||
this.textView = textView;
|
||||
textView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
playerManager.selectQueueItem(getAdapterPosition());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
|
||||
|
||||
@Override
|
||||
|
|
@ -186,11 +207,14 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||
|
||||
@Override
|
||||
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
|
||||
holder.item = playerManager.getItem(position);
|
||||
TextView view = holder.textView;
|
||||
view.setText(playerManager.getItem(position).name);
|
||||
view.setText(holder.item.title);
|
||||
// TODO: Solve coloring using the theme's ColorStateList.
|
||||
view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(),
|
||||
position == playerManager.getCurrentItemIndex() ? 255 : 100));
|
||||
view.setTextColor(
|
||||
ColorUtils.setAlphaComponent(
|
||||
view.getCurrentTextColor(),
|
||||
position == playerManager.getCurrentItemIndex() ? 255 : 100));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -228,8 +252,11 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
if (playerManager.removeItem(position)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -237,8 +264,9 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||
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(draggingFromPosition, draggingToPosition)) {
|
||||
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();
|
||||
|
|
@ -247,15 +275,37 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||
draggingFromPosition = C.INDEX_UNSET;
|
||||
draggingToPosition = C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class SampleListAdapter extends ArrayAdapter<Sample> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
* 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.
|
||||
|
|
@ -20,108 +20,102 @@ import android.net.Uri;
|
|||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Player.DefaultEventListener;
|
||||
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.RenderersFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
|
||||
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.ExtractorMediaSource;
|
||||
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.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.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.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaMetadata;
|
||||
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 ExoPlayer/Cast demo app.
|
||||
*/
|
||||
/* package */ final class PlayerManager extends DefaultEventListener
|
||||
implements CastPlayer.SessionAvailabilityListener {
|
||||
/** Manages players and an internal media queue for the demo app. */
|
||||
/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener {
|
||||
|
||||
/**
|
||||
* Listener for changes in the media queue playback position.
|
||||
*/
|
||||
public interface QueuePositionListener {
|
||||
/** Listener for events. */
|
||||
interface Listener {
|
||||
|
||||
/**
|
||||
* Called when the currently played item of the media queue changes.
|
||||
*/
|
||||
/** 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 DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
||||
new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER);
|
||||
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<DemoUtil.Sample> mediaQueue;
|
||||
private final QueuePositionListener queuePositionListener;
|
||||
private final ArrayList<MediaItem> mediaQueue;
|
||||
private final Listener listener;
|
||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||
private final MediaItemConverter mediaItemConverter;
|
||||
|
||||
private boolean castMediaQueueCreationPending;
|
||||
private TrackGroupArray lastSeenTrackGroupArray;
|
||||
private int currentItemIndex;
|
||||
private Player currentPlayer;
|
||||
|
||||
/**
|
||||
* @param queuePositionListener A {@link QueuePositionListener} for queue position changes.
|
||||
* 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 static PlayerManager createPlayerManager(
|
||||
QueuePositionListener queuePositionListener,
|
||||
public PlayerManager(
|
||||
Listener listener,
|
||||
PlayerView localPlayerView,
|
||||
PlayerControlView castControlView,
|
||||
Context context,
|
||||
CastContext castContext) {
|
||||
PlayerManager playerManager =
|
||||
new PlayerManager(
|
||||
queuePositionListener, localPlayerView, castControlView, context, castContext);
|
||||
playerManager.init();
|
||||
return playerManager;
|
||||
}
|
||||
|
||||
private PlayerManager(
|
||||
QueuePositionListener queuePositionListener,
|
||||
PlayerView localPlayerView,
|
||||
PlayerControlView castControlView,
|
||||
Context context,
|
||||
CastContext castContext) {
|
||||
this.queuePositionListener = queuePositionListener;
|
||||
this.listener = listener;
|
||||
this.localPlayerView = localPlayerView;
|
||||
this.castControlView = castControlView;
|
||||
mediaQueue = new ArrayList<>();
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||
mediaItemConverter = new DefaultMediaItemConverter();
|
||||
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
|
||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null);
|
||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
||||
trackSelector = new DefaultTrackSelector(context);
|
||||
exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build();
|
||||
exoPlayer.addListener(this);
|
||||
localPlayerView.setPlayer(exoPlayer);
|
||||
|
||||
|
|
@ -129,6 +123,8 @@ import java.util.ArrayList;
|
|||
castPlayer.addListener(this);
|
||||
castPlayer.setSessionAvailabilityListener(this);
|
||||
castControlView.setPlayer(castPlayer);
|
||||
|
||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
||||
}
|
||||
|
||||
// Queue manipulation methods.
|
||||
|
|
@ -142,29 +138,25 @@ import java.util.ArrayList;
|
|||
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the currently played item.
|
||||
*/
|
||||
/** Returns the index of the currently played item. */
|
||||
public int getCurrentItemIndex() {
|
||||
return currentItemIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends {@code sample} to the media queue.
|
||||
* Appends {@code item} to the media queue.
|
||||
*
|
||||
* @param sample The {@link Sample} to append.
|
||||
* @param item The {@link MediaItem} to append.
|
||||
*/
|
||||
public void addItem(Sample sample) {
|
||||
mediaQueue.add(sample);
|
||||
concatenatingMediaSource.addMediaSource(buildMediaSource(sample));
|
||||
public void addItem(MediaItem item) {
|
||||
mediaQueue.add(item);
|
||||
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
||||
if (currentPlayer == castPlayer) {
|
||||
castPlayer.addItems(buildMediaQueueItem(sample));
|
||||
castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the media queue.
|
||||
*/
|
||||
/** Returns the size of the media queue. */
|
||||
public int getMediaQueueSize() {
|
||||
return mediaQueue.size();
|
||||
}
|
||||
|
|
@ -175,17 +167,21 @@ import java.util.ArrayList;
|
|||
* @param position The index of the item.
|
||||
* @return The item at the given index in the media queue.
|
||||
*/
|
||||
public Sample getItem(int position) {
|
||||
public MediaItem getItem(int position) {
|
||||
return mediaQueue.get(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the item at the given index from the media queue.
|
||||
*
|
||||
* @param itemIndex The index of the item to remove.
|
||||
* @param item The item to remove.
|
||||
* @return Whether the removal was successful.
|
||||
*/
|
||||
public boolean removeItem(int itemIndex) {
|
||||
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) {
|
||||
|
|
@ -208,11 +204,15 @@ import java.util.ArrayList;
|
|||
/**
|
||||
* Moves an item within the queue.
|
||||
*
|
||||
* @param fromIndex The index of the item to move.
|
||||
* @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(int fromIndex, int toIndex) {
|
||||
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) {
|
||||
|
|
@ -239,8 +239,6 @@ import java.util.ArrayList;
|
|||
return true;
|
||||
}
|
||||
|
||||
// Miscellaneous methods.
|
||||
|
||||
/**
|
||||
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
|
||||
*
|
||||
|
|
@ -255,9 +253,7 @@ import java.util.ArrayList;
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the manager and the players that it holds.
|
||||
*/
|
||||
/** Releases the manager and the players that it holds. */
|
||||
public void release() {
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
mediaQueue.clear();
|
||||
|
|
@ -271,7 +267,7 @@ import java.util.ArrayList;
|
|||
// Player.EventListener implementation.
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
|
|
@ -281,11 +277,26 @@ import java.util.ArrayList;
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(
|
||||
Timeline timeline, Object manifest, @TimelineChangeReason int reason) {
|
||||
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
|
||||
updateCurrentItemIndex();
|
||||
if (timeline.isEmpty()) {
|
||||
castMediaQueueCreationPending = true;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -303,15 +314,12 @@ import java.util.ArrayList;
|
|||
|
||||
// Internal methods.
|
||||
|
||||
private void init() {
|
||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
||||
}
|
||||
|
||||
private void updateCurrentItemIndex() {
|
||||
int playbackState = currentPlayer.getPlaybackState();
|
||||
maybeSetCurrentItemAndNotify(
|
||||
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
||||
? currentPlayer.getCurrentWindowIndex() : C.INDEX_UNSET);
|
||||
? currentPlayer.getCurrentWindowIndex()
|
||||
: C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
private void setCurrentPlayer(Player currentPlayer) {
|
||||
|
|
@ -332,26 +340,26 @@ import java.util.ArrayList;
|
|||
long playbackPositionMs = C.TIME_UNSET;
|
||||
int windowIndex = C.INDEX_UNSET;
|
||||
boolean playWhenReady = false;
|
||||
if (this.currentPlayer != null) {
|
||||
int playbackState = this.currentPlayer.getPlaybackState();
|
||||
|
||||
Player previousPlayer = this.currentPlayer;
|
||||
if (previousPlayer != null) {
|
||||
// Save state from the previous player.
|
||||
int playbackState = previousPlayer.getPlaybackState();
|
||||
if (playbackState != Player.STATE_ENDED) {
|
||||
playbackPositionMs = this.currentPlayer.getCurrentPosition();
|
||||
playWhenReady = this.currentPlayer.getPlayWhenReady();
|
||||
windowIndex = this.currentPlayer.getCurrentWindowIndex();
|
||||
playbackPositionMs = previousPlayer.getCurrentPosition();
|
||||
playWhenReady = previousPlayer.getPlayWhenReady();
|
||||
windowIndex = previousPlayer.getCurrentWindowIndex();
|
||||
if (windowIndex != currentItemIndex) {
|
||||
playbackPositionMs = C.TIME_UNSET;
|
||||
windowIndex = currentItemIndex;
|
||||
}
|
||||
}
|
||||
this.currentPlayer.stop(true);
|
||||
} else {
|
||||
// This is the initial setup. No need to save any state.
|
||||
previousPlayer.stop(true);
|
||||
}
|
||||
|
||||
this.currentPlayer = currentPlayer;
|
||||
|
||||
// Media queue management.
|
||||
castMediaQueueCreationPending = currentPlayer == castPlayer;
|
||||
if (currentPlayer == exoPlayer) {
|
||||
exoPlayer.prepare(concatenatingMediaSource);
|
||||
}
|
||||
|
|
@ -371,12 +379,11 @@ import java.util.ArrayList;
|
|||
*/
|
||||
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
||||
maybeSetCurrentItemAndNotify(itemIndex);
|
||||
if (castMediaQueueCreationPending) {
|
||||
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
|
||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = buildMediaQueueItem(mediaQueue.get(i));
|
||||
items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i));
|
||||
}
|
||||
castMediaQueueCreationPending = false;
|
||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
||||
} else {
|
||||
currentPlayer.seekTo(itemIndex, positionMs);
|
||||
|
|
@ -388,38 +395,65 @@ import java.util.ArrayList;
|
|||
if (this.currentItemIndex != currentItemIndex) {
|
||||
int oldIndex = this.currentItemIndex;
|
||||
this.currentItemIndex = currentItemIndex;
|
||||
queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
||||
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaSource buildMediaSource(DemoUtil.Sample sample) {
|
||||
Uri uri = Uri.parse(sample.uri);
|
||||
switch (sample.mimeType) {
|
||||
case DemoUtil.MIME_TYPE_SS:
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY)
|
||||
.createMediaSource(uri);
|
||||
case DemoUtil.MIME_TYPE_DASH:
|
||||
return new DashMediaSource.Factory(
|
||||
new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY)
|
||||
.createMediaSource(uri);
|
||||
case DemoUtil.MIME_TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||
return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||
default: {
|
||||
throw new IllegalStateException("Unsupported type: " + sample.mimeType);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) {
|
||||
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
||||
movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name);
|
||||
MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri)
|
||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType)
|
||||
.setMetadata(movieMetadata).build();
|
||||
return new MediaQueueItem.Builder(mediaInfo).build();
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,12 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<vector android:alpha="0.8" android:height="24dp" android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0" android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
|
||||
<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>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?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.
|
||||
|
|
@ -14,8 +13,10 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_all_description">"Bütün təkrarlayın"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Təkrar bir"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Heç bir təkrar"</string>
|
||||
</resources>
|
||||
<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"/>
|
||||
|
|
@ -19,34 +19,42 @@
|
|||
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="12"
|
||||
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="12">
|
||||
<android.support.v7.widget.RecyclerView android:id="@+id/sample_list"
|
||||
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"/>
|
||||
<ImageButton android:id="@+id/add_sample_button"
|
||||
android:background="@drawable/ic_add_circle_white_24dp"
|
||||
|
||||
<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:padding="30dp"/>
|
||||
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="0dp"
|
||||
android:layout_weight="2"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:repeat_toggle_modes="all|one"
|
||||
app:show_timeout="-1"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
limitations under the License.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ListView android:id="@+id/sample_list"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<item
|
||||
android:id="@+id/media_route_menu_item"
|
||||
android:title="@string/media_route_menu_title"
|
||||
app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
|
||||
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@
|
|||
|
||||
<string name="media_route_menu_title">Cast</string>
|
||||
|
||||
<string name="sample_list_dialog_title">Add samples</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>
|
||||
|
|
|
|||
11
demos/gl/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# ExoPlayer GL demo
|
||||
|
||||
This app demonstrates how to render video to a [GLSurfaceView][] while applying
|
||||
a GL shader.
|
||||
|
||||
The shader shows an overlap bitmap on top of the video. The overlay bitmap is
|
||||
drawn using an Android canvas, and includes the current frame's presentation
|
||||
timestamp, to show how to get the timestamp of the frame currently in the
|
||||
off-screen surface texture.
|
||||
|
||||
[GLSurfaceView]: https://developer.android.com/reference/android/opengl/GLSurfaceView
|
||||
53
demos/gl/build.gradle
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (C) 2020 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 getDefaultProguardFile('proguard-android.txt')
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// This demo app does not have translations.
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||
}
|
||||
49
demos/gl/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2020 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.gldemo">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/application_name">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<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.gldemo.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>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2020 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.
|
||||
|
||||
#extension GL_OES_EGL_image_external : require
|
||||
precision mediump float;
|
||||
// External texture containing video decoder output.
|
||||
uniform samplerExternalOES tex_sampler_0;
|
||||
// Texture containing the overlap bitmap.
|
||||
uniform sampler2D tex_sampler_1;
|
||||
// Horizontal scaling factor for the overlap bitmap.
|
||||
uniform float scaleX;
|
||||
// Vertical scaling factory for the overlap bitmap.
|
||||
uniform float scaleY;
|
||||
varying vec2 v_texcoord;
|
||||
void main() {
|
||||
vec4 videoColor = texture2D(tex_sampler_0, v_texcoord);
|
||||
vec4 overlayColor = texture2D(tex_sampler_1,
|
||||
vec2(v_texcoord.x * scaleX,
|
||||
v_texcoord.y * scaleY));
|
||||
// Blend the video decoder output and the overlay bitmap.
|
||||
gl_FragColor = videoColor * (1.0 - overlayColor.a)
|
||||
+ overlayColor * overlayColor.a;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2020 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.
|
||||
attribute vec4 a_position;
|
||||
attribute vec3 a_texcoord;
|
||||
varying vec2 v_texcoord;
|
||||
void main() {
|
||||
gl_Position = a_position;
|
||||
v_texcoord = a_texcoord.xy;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* Copyright (C) 2020 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.gldemo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.GLUtils;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.GlUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Locale;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
/**
|
||||
* Video processor that demonstrates how to overlay a bitmap on video output using a GL shader. The
|
||||
* bitmap is drawn using an Android {@link Canvas}.
|
||||
*/
|
||||
/* package */ final class BitmapOverlayVideoProcessor
|
||||
implements VideoProcessingGLSurfaceView.VideoProcessor {
|
||||
|
||||
private static final int OVERLAY_WIDTH = 512;
|
||||
private static final int OVERLAY_HEIGHT = 256;
|
||||
|
||||
private final Context context;
|
||||
private final Paint paint;
|
||||
private final int[] textures;
|
||||
private final Bitmap overlayBitmap;
|
||||
private final Bitmap logoBitmap;
|
||||
private final Canvas overlayCanvas;
|
||||
|
||||
private int program;
|
||||
@Nullable private GlUtil.Attribute[] attributes;
|
||||
@Nullable private GlUtil.Uniform[] uniforms;
|
||||
|
||||
private float bitmapScaleX;
|
||||
private float bitmapScaleY;
|
||||
|
||||
public BitmapOverlayVideoProcessor(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
paint = new Paint();
|
||||
paint.setTextSize(64);
|
||||
paint.setAntiAlias(true);
|
||||
paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF);
|
||||
textures = new int[1];
|
||||
overlayBitmap = Bitmap.createBitmap(OVERLAY_WIDTH, OVERLAY_HEIGHT, Bitmap.Config.ARGB_8888);
|
||||
overlayCanvas = new Canvas(overlayBitmap);
|
||||
try {
|
||||
logoBitmap =
|
||||
((BitmapDrawable)
|
||||
context.getPackageManager().getApplicationIcon(context.getPackageName()))
|
||||
.getBitmap();
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
String vertexShaderCode =
|
||||
loadAssetAsString(context, "bitmap_overlay_video_processor_vertex.glsl");
|
||||
String fragmentShaderCode =
|
||||
loadAssetAsString(context, "bitmap_overlay_video_processor_fragment.glsl");
|
||||
program = GlUtil.compileProgram(vertexShaderCode, fragmentShaderCode);
|
||||
GlUtil.Attribute[] attributes = GlUtil.getAttributes(program);
|
||||
GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program);
|
||||
for (GlUtil.Attribute attribute : attributes) {
|
||||
if (attribute.name.equals("a_position")) {
|
||||
attribute.setBuffer(
|
||||
new float[] {
|
||||
-1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
|
||||
1.0f, 0.0f, 1.0f,
|
||||
},
|
||||
4);
|
||||
} else if (attribute.name.equals("a_texcoord")) {
|
||||
attribute.setBuffer(
|
||||
new float[] {
|
||||
0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,
|
||||
},
|
||||
3);
|
||||
}
|
||||
}
|
||||
this.attributes = attributes;
|
||||
this.uniforms = uniforms;
|
||||
GLES20.glGenTextures(1, textures, 0);
|
||||
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
|
||||
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
|
||||
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
|
||||
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT);
|
||||
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);
|
||||
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSurfaceSize(int width, int height) {
|
||||
bitmapScaleX = (float) width / OVERLAY_WIDTH;
|
||||
bitmapScaleY = (float) height / OVERLAY_HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(int frameTexture, long frameTimestampUs) {
|
||||
// Draw to the canvas and store it in a texture.
|
||||
String text = String.format(Locale.US, "%.02f", frameTimestampUs / (float) C.MICROS_PER_SECOND);
|
||||
overlayBitmap.eraseColor(Color.TRANSPARENT);
|
||||
overlayCanvas.drawBitmap(logoBitmap, /* left= */ 32, /* top= */ 32, paint);
|
||||
overlayCanvas.drawText(text, /* x= */ 200, /* y= */ 130, paint);
|
||||
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
|
||||
GLUtils.texSubImage2D(
|
||||
GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap);
|
||||
GlUtil.checkGlError();
|
||||
|
||||
// Run the shader program.
|
||||
GlUtil.Uniform[] uniforms = Assertions.checkNotNull(this.uniforms);
|
||||
GlUtil.Attribute[] attributes = Assertions.checkNotNull(this.attributes);
|
||||
GLES20.glUseProgram(program);
|
||||
for (GlUtil.Uniform uniform : uniforms) {
|
||||
switch (uniform.name) {
|
||||
case "tex_sampler_0":
|
||||
uniform.setSamplerTexId(frameTexture, /* unit= */ 0);
|
||||
break;
|
||||
case "tex_sampler_1":
|
||||
uniform.setSamplerTexId(textures[0], /* unit= */ 1);
|
||||
break;
|
||||
case "scaleX":
|
||||
uniform.setFloat(bitmapScaleX);
|
||||
break;
|
||||
case "scaleY":
|
||||
uniform.setFloat(bitmapScaleY);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (GlUtil.Attribute copyExternalAttribute : attributes) {
|
||||
copyExternalAttribute.bind();
|
||||
}
|
||||
for (GlUtil.Uniform copyExternalUniform : uniforms) {
|
||||
copyExternalUniform.bind();
|
||||
}
|
||||
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||
GlUtil.checkGlError();
|
||||
}
|
||||
|
||||
private static String loadAssetAsString(Context context, String assetFileName) {
|
||||
@Nullable InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = context.getAssets().open(assetFileName);
|
||||
return Util.fromUtf8Bytes(Util.toByteArray(inputStream));
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
Util.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* Copyright (C) 2020 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.gldemo;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
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.PlayerView;
|
||||
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.EventLogger;
|
||||
import com.google.android.exoplayer2.util.GlUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Activity that demonstrates playback of video to an {@link android.opengl.GLSurfaceView} with
|
||||
* postprocessing of the video content using GL.
|
||||
*/
|
||||
public final class MainActivity extends Activity {
|
||||
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
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 ACTION_VIEW = "com.google.android.exoplayer.gldemo.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";
|
||||
|
||||
@Nullable private PlayerView playerView;
|
||||
@Nullable private VideoProcessingGLSurfaceView videoProcessingGLSurfaceView;
|
||||
|
||||
@Nullable private SimpleExoPlayer player;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main_activity);
|
||||
playerView = findViewById(R.id.player_view);
|
||||
|
||||
Context context = getApplicationContext();
|
||||
boolean requestSecureSurface = getIntent().hasExtra(DRM_SCHEME_EXTRA);
|
||||
if (requestSecureSurface && !GlUtil.isProtectedContentExtensionSupported(context)) {
|
||||
Toast.makeText(
|
||||
context, R.string.error_protected_content_extension_not_supported, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
|
||||
VideoProcessingGLSurfaceView videoProcessingGLSurfaceView =
|
||||
new VideoProcessingGLSurfaceView(
|
||||
context, requestSecureSurface, new BitmapOverlayVideoProcessor(context));
|
||||
FrameLayout contentFrame = findViewById(R.id.exo_content_frame);
|
||||
contentFrame.addView(videoProcessingGLSurfaceView);
|
||||
this.videoProcessingGLSurfaceView = videoProcessingGLSurfaceView;
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
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 (Util.SDK_INT >= 18 && 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.setRepeatMode(Player.REPEAT_MODE_ALL);
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
player.play();
|
||||
VideoProcessingGLSurfaceView videoProcessingGLSurfaceView =
|
||||
Assertions.checkNotNull(this.videoProcessingGLSurfaceView);
|
||||
videoProcessingGLSurfaceView.setVideoComponent(
|
||||
Assertions.checkNotNull(player.getVideoComponent()));
|
||||
Assertions.checkNotNull(playerView).setPlayer(player);
|
||||
player.addAnalyticsListener(new EventLogger(/* trackSelector= */ null));
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
Assertions.checkNotNull(playerView).setPlayer(null);
|
||||
if (player != null) {
|
||||
player.release();
|
||||
Assertions.checkNotNull(videoProcessingGLSurfaceView).setVideoComponent(null);
|
||||
player = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
* Copyright (C) 2020 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.gldemo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.media.MediaFormat;
|
||||
import android.opengl.EGL14;
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.os.Handler;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.GlUtil;
|
||||
import com.google.android.exoplayer2.util.TimedValueQueue;
|
||||
import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.egl.EGLContext;
|
||||
import javax.microedition.khronos.egl.EGLDisplay;
|
||||
import javax.microedition.khronos.egl.EGLSurface;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
/**
|
||||
* {@link GLSurfaceView} that creates a GL context (optionally for protected content) and passes
|
||||
* video frames to a {@link VideoProcessor} for drawing to the view.
|
||||
*
|
||||
* <p>This view must be created programmatically, as it is necessary to specify whether a context
|
||||
* supporting protected content should be created at construction time.
|
||||
*/
|
||||
public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
|
||||
|
||||
/** Processes video frames, provided via a GL texture. */
|
||||
public interface VideoProcessor {
|
||||
/** Performs any required GL initialization. */
|
||||
void initialize();
|
||||
|
||||
/** Sets the size of the output surface in pixels. */
|
||||
void setSurfaceSize(int width, int height);
|
||||
|
||||
/**
|
||||
* Draws using GL operations.
|
||||
*
|
||||
* @param frameTexture The ID of a GL texture containing a video frame.
|
||||
* @param frameTimestampUs The presentation timestamp of the frame, in microseconds.
|
||||
*/
|
||||
void draw(int frameTexture, long frameTimestampUs);
|
||||
}
|
||||
|
||||
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
|
||||
|
||||
private final VideoRenderer renderer;
|
||||
private final Handler mainHandler;
|
||||
|
||||
@Nullable private SurfaceTexture surfaceTexture;
|
||||
@Nullable private Surface surface;
|
||||
@Nullable private Player.VideoComponent videoComponent;
|
||||
|
||||
/**
|
||||
* Creates a new instance. Pass {@code true} for {@code requireSecureContext} if the {@link
|
||||
* GLSurfaceView GLSurfaceView's} associated GL context should handle secure content (if the
|
||||
* device supports it).
|
||||
*
|
||||
* @param context The {@link Context}.
|
||||
* @param requireSecureContext Whether a GL context supporting protected content should be
|
||||
* created, if supported by the device.
|
||||
* @param videoProcessor Processor that draws to the view.
|
||||
*/
|
||||
public VideoProcessingGLSurfaceView(
|
||||
Context context, boolean requireSecureContext, VideoProcessor videoProcessor) {
|
||||
super(context);
|
||||
renderer = new VideoRenderer(videoProcessor);
|
||||
mainHandler = new Handler();
|
||||
setEGLContextClientVersion(2);
|
||||
setEGLConfigChooser(
|
||||
/* redSize= */ 8,
|
||||
/* greenSize= */ 8,
|
||||
/* blueSize= */ 8,
|
||||
/* alphaSize= */ 8,
|
||||
/* depthSize= */ 0,
|
||||
/* stencilSize= */ 0);
|
||||
setEGLContextFactory(
|
||||
new EGLContextFactory() {
|
||||
@Override
|
||||
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
|
||||
int[] glAttributes;
|
||||
if (requireSecureContext) {
|
||||
glAttributes =
|
||||
new int[] {
|
||||
EGL14.EGL_CONTEXT_CLIENT_VERSION,
|
||||
2,
|
||||
EGL_PROTECTED_CONTENT_EXT,
|
||||
EGL14.EGL_TRUE,
|
||||
EGL14.EGL_NONE
|
||||
};
|
||||
} else {
|
||||
glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
|
||||
}
|
||||
return egl.eglCreateContext(
|
||||
display, eglConfig, /* share_context= */ EGL10.EGL_NO_CONTEXT, glAttributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
|
||||
egl.eglDestroyContext(display, context);
|
||||
}
|
||||
});
|
||||
setEGLWindowSurfaceFactory(
|
||||
new EGLWindowSurfaceFactory() {
|
||||
@Override
|
||||
public EGLSurface createWindowSurface(
|
||||
EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) {
|
||||
int[] attribsList =
|
||||
requireSecureContext
|
||||
? new int[] {EGL_PROTECTED_CONTENT_EXT, EGL14.EGL_TRUE, EGL10.EGL_NONE}
|
||||
: new int[] {EGL10.EGL_NONE};
|
||||
return egl.eglCreateWindowSurface(display, config, nativeWindow, attribsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) {
|
||||
egl.eglDestroySurface(display, surface);
|
||||
}
|
||||
});
|
||||
setRenderer(renderer);
|
||||
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches or detaches (if {@code newVideoComponent} is {@code null}) this view from the video
|
||||
* component of the player.
|
||||
*
|
||||
* @param newVideoComponent The new video component, or {@code null} to detach this view.
|
||||
*/
|
||||
public void setVideoComponent(@Nullable Player.VideoComponent newVideoComponent) {
|
||||
if (newVideoComponent == videoComponent) {
|
||||
return;
|
||||
}
|
||||
if (videoComponent != null) {
|
||||
if (surface != null) {
|
||||
videoComponent.clearVideoSurface(surface);
|
||||
}
|
||||
videoComponent.clearVideoFrameMetadataListener(renderer);
|
||||
}
|
||||
videoComponent = newVideoComponent;
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoFrameMetadataListener(renderer);
|
||||
videoComponent.setVideoSurface(surface);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
// Post to make sure we occur in order with any onSurfaceTextureAvailable calls.
|
||||
mainHandler.post(
|
||||
() -> {
|
||||
if (surface != null) {
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoSurface(null);
|
||||
}
|
||||
releaseSurface(surfaceTexture, surface);
|
||||
surfaceTexture = null;
|
||||
surface = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {
|
||||
mainHandler.post(
|
||||
() -> {
|
||||
SurfaceTexture oldSurfaceTexture = this.surfaceTexture;
|
||||
Surface oldSurface = VideoProcessingGLSurfaceView.this.surface;
|
||||
this.surfaceTexture = surfaceTexture;
|
||||
this.surface = new Surface(surfaceTexture);
|
||||
releaseSurface(oldSurfaceTexture, oldSurface);
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoSurface(surface);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void releaseSurface(
|
||||
@Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) {
|
||||
if (oldSurfaceTexture != null) {
|
||||
oldSurfaceTexture.release();
|
||||
}
|
||||
if (oldSurface != null) {
|
||||
oldSurface.release();
|
||||
}
|
||||
}
|
||||
|
||||
private final class VideoRenderer implements GLSurfaceView.Renderer, VideoFrameMetadataListener {
|
||||
|
||||
private final VideoProcessor videoProcessor;
|
||||
private final AtomicBoolean frameAvailable;
|
||||
private final TimedValueQueue<Long> sampleTimestampQueue;
|
||||
|
||||
private int texture;
|
||||
@Nullable private SurfaceTexture surfaceTexture;
|
||||
|
||||
private boolean initialized;
|
||||
private int width;
|
||||
private int height;
|
||||
private long frameTimestampUs;
|
||||
|
||||
public VideoRenderer(VideoProcessor videoProcessor) {
|
||||
this.videoProcessor = videoProcessor;
|
||||
frameAvailable = new AtomicBoolean();
|
||||
sampleTimestampQueue = new TimedValueQueue<>();
|
||||
width = -1;
|
||||
height = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) {
|
||||
texture = GlUtil.createExternalTexture();
|
||||
surfaceTexture = new SurfaceTexture(texture);
|
||||
surfaceTexture.setOnFrameAvailableListener(
|
||||
surfaceTexture -> {
|
||||
frameAvailable.set(true);
|
||||
requestRender();
|
||||
});
|
||||
onSurfaceTextureAvailable(surfaceTexture);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceChanged(GL10 gl, int width, int height) {
|
||||
GLES20.glViewport(0, 0, width, height);
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawFrame(GL10 gl) {
|
||||
if (videoProcessor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initialized) {
|
||||
videoProcessor.initialize();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
if (width != -1 && height != -1) {
|
||||
videoProcessor.setSurfaceSize(width, height);
|
||||
width = -1;
|
||||
height = -1;
|
||||
}
|
||||
|
||||
if (frameAvailable.compareAndSet(true, false)) {
|
||||
SurfaceTexture surfaceTexture = Assertions.checkNotNull(this.surfaceTexture);
|
||||
surfaceTexture.updateTexImage();
|
||||
long lastFrameTimestampNs = surfaceTexture.getTimestamp();
|
||||
Long frameTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs);
|
||||
if (frameTimestampUs != null) {
|
||||
this.frameTimestampUs = frameTimestampUs;
|
||||
}
|
||||
}
|
||||
|
||||
videoProcessor.draw(texture, frameTimestampUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoFrameAboutToBeRendered(
|
||||
long presentationTimeUs,
|
||||
long releaseTimeNs,
|
||||
Format format,
|
||||
@Nullable MediaFormat mediaFormat) {
|
||||
sampleTimestampQueue.add(releaseTimeNs, presentationTimeUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
demos/gl/src/main/res/layout/main_activity.xml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2020 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">
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerView
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:surface_type="none"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
<!-- Copyright (C) 2020 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.
|
||||
|
|
@ -13,11 +13,10 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<resources>
|
||||
|
||||
<style name="PlayerTheme" parent="android:Theme.Holo">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
<string name="application_name">ExoPlayer GL demo</string>
|
||||
|
||||
<string name="error_protected_content_extension_not_supported">The GL protected content extension is not supported.</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# IMA demo application #
|
||||
|
||||
This folder contains a demo application that showcases ExoPlayer integration
|
||||
with the IMA SDK.
|
||||
|
|
@ -1,58 +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.imademo;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
|
||||
/**
|
||||
* Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by
|
||||
* {@link PlayerManager}, which this class instantiates.
|
||||
*/
|
||||
public final class MainActivity extends Activity {
|
||||
|
||||
private PlayerView playerView;
|
||||
private PlayerManager player;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main_activity);
|
||||
playerView = findViewById(R.id.player_view);
|
||||
player = new PlayerManager(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
player.init(this, playerView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
player.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
player.release();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,153 +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.imademo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.C.ContentType;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
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.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
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.util.Util;
|
||||
|
||||
/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */
|
||||
/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {
|
||||
|
||||
private final ImaAdsLoader adsLoader;
|
||||
private final DataSource.Factory manifestDataSourceFactory;
|
||||
private final DataSource.Factory mediaDataSourceFactory;
|
||||
|
||||
private SimpleExoPlayer player;
|
||||
private long contentPosition;
|
||||
|
||||
public PlayerManager(Context context) {
|
||||
String adTag = context.getString(R.string.ad_tag_url);
|
||||
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
|
||||
manifestDataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
context, Util.getUserAgent(context, context.getString(R.string.application_name)));
|
||||
mediaDataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
context,
|
||||
Util.getUserAgent(context, context.getString(R.string.application_name)),
|
||||
new DefaultBandwidthMeter());
|
||||
}
|
||||
|
||||
public void init(Context context, PlayerView playerView) {
|
||||
// Create a default track selector.
|
||||
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
TrackSelection.Factory videoTrackSelectionFactory =
|
||||
new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
||||
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
|
||||
|
||||
// Create a player instance.
|
||||
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
|
||||
|
||||
// Bind the player to the view.
|
||||
playerView.setPlayer(player);
|
||||
|
||||
// This is the MediaSource representing the content media (i.e. not the ad).
|
||||
String contentUrl = context.getString(R.string.content_url);
|
||||
MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl));
|
||||
|
||||
// Compose the content media source into a new AdsMediaSource with both ads and content.
|
||||
MediaSource mediaSourceWithAds =
|
||||
new AdsMediaSource(
|
||||
contentMediaSource,
|
||||
/* adMediaSourceFactory= */ this,
|
||||
adsLoader,
|
||||
playerView.getOverlayFrameLayout(),
|
||||
/* eventHandler= */ null,
|
||||
/* eventListener= */ null);
|
||||
|
||||
// Prepare the player with the source.
|
||||
player.seekTo(contentPosition);
|
||||
player.prepare(mediaSourceWithAds);
|
||||
player.setPlayWhenReady(true);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
if (player != null) {
|
||||
contentPosition = player.getContentPosition();
|
||||
player.release();
|
||||
player = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (player != null) {
|
||||
player.release();
|
||||
player = null;
|
||||
}
|
||||
adsLoader.release();
|
||||
}
|
||||
|
||||
// AdsMediaSource.MediaSourceFactory implementation.
|
||||
|
||||
@Override
|
||||
public MediaSource createMediaSource(Uri uri) {
|
||||
return buildMediaSource(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedTypes() {
|
||||
// IMA does not support Smooth Streaming ads.
|
||||
return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private MediaSource buildMediaSource(Uri uri) {
|
||||
@ContentType int type = Util.inferContentType(uri);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(
|
||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
||||
manifestDataSourceFactory)
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,13 +16,17 @@ apply plugin: 'com.android.application'
|
|||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion 16
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -40,8 +44,9 @@ android {
|
|||
}
|
||||
|
||||
lintOptions {
|
||||
// The demo app does not have translations.
|
||||
disable 'MissingTranslation'
|
||||
// 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"
|
||||
|
|
@ -57,12 +62,15 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
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')
|
||||
|
|
@ -70,3 +78,5 @@ dependencies {
|
|||
withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp')
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||
|
|
|
|||
|
|
@ -15,10 +15,15 @@
|
|||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.demo">
|
||||
|
||||
<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.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.hardware.touchscreen" android:required="false"/>
|
||||
<uses-sdk/>
|
||||
|
|
@ -29,11 +34,14 @@
|
|||
android:banner="@drawable/ic_banner"
|
||||
android:largeHeap="true"
|
||||
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"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:label="@string/application_name">
|
||||
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"/>
|
||||
|
|
@ -73,6 +81,18 @@
|
|||
</intent-filter>
|
||||
</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>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -4,22 +4,22 @@
|
|||
"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",
|
||||
"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": "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",
|
||||
"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": "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",
|
||||
"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": "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",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
|
@ -208,6 +208,13 @@
|
|||
"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"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -330,11 +337,11 @@
|
|||
"samples": [
|
||||
{
|
||||
"name": "Super speed",
|
||||
"uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism"
|
||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
|
||||
},
|
||||
{
|
||||
"name": "Super speed (PlayReady)",
|
||||
"uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
|
||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
|
||||
"drm_scheme": "playready"
|
||||
}
|
||||
]
|
||||
|
|
@ -352,11 +359,11 @@
|
|||
},
|
||||
{
|
||||
"name": "Apple master playlist advanced (TS)",
|
||||
"uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_example_v2/master.m3u8"
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8"
|
||||
},
|
||||
{
|
||||
"name": "Apple master playlist advanced (fMP4)",
|
||||
"uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_fmp4_example/master.m3u8"
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
|
||||
},
|
||||
{
|
||||
"name": "Apple TS media playlist",
|
||||
|
|
@ -365,10 +372,6 @@
|
|||
{
|
||||
"name": "Apple AAC media playlist",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
|
||||
},
|
||||
{
|
||||
"name": "Apple ID3 metadata",
|
||||
"uri": "http://devimages.apple.com/samplecode/adDemo/ad.m3u8"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -376,64 +379,56 @@
|
|||
"name": "Misc",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Dizzy",
|
||||
"name": "Dizzy (MP4)",
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"name": "Apple AAC 10s",
|
||||
"name": "Apple 10s (AAC)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
|
||||
},
|
||||
{
|
||||
"name": "Apple TS 10s",
|
||||
"name": "Apple 10s (TS)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
|
||||
},
|
||||
{
|
||||
"name": "Android screens (Matroska)",
|
||||
"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": "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)",
|
||||
"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 (FMP4,H264,No Audio)",
|
||||
"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 (FMP4,H264, No Audio)",
|
||||
"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 (FMP4,AAC Audio)",
|
||||
"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 Audio)",
|
||||
"name": "Google Play (MP3)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
|
||||
},
|
||||
{
|
||||
"name": "Google Play (Ogg/Vorbis Audio)",
|
||||
"name": "Google Play (Ogg/Vorbis)",
|
||||
"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 Play (FLAC)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/flac/play.flac"
|
||||
},
|
||||
{
|
||||
"name": "Google Glass (VP9 in MP4/ISO-BMFF)",
|
||||
"uri": "http://demos.webmproject.org/exoplayer/glass.mp4"
|
||||
"name": "Big Buck Bunny video (FLV)",
|
||||
"uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
|
||||
},
|
||||
{
|
||||
"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": "Big Buck Bunny 480p video (MP4,AV1)",
|
||||
"uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -467,23 +462,27 @@
|
|||
},
|
||||
{
|
||||
"name": "Clear -> Enc -> Clear -> Enc -> Enc",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://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",
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -570,12 +569,51 @@
|
|||
{
|
||||
"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": "http://vastsynthesizer.appspot.com/empty-midroll"
|
||||
"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": "http://vastsynthesizer.appspot.com/empty-midroll-2"
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,20 +16,52 @@
|
|||
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.ui.DownloadNotificationHelper;
|
||||
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.TransferListener;
|
||||
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 {
|
||||
|
||||
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
|
||||
|
||||
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;
|
||||
private DownloadNotificationHelper downloadNotificationHelper;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
|
@ -37,18 +69,117 @@ public class DemoApplication extends Application {
|
|||
}
|
||||
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) {
|
||||
return new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
|
||||
public DataSource.Factory buildDataSourceFactory() {
|
||||
DefaultDataSourceFactory upstreamFactory =
|
||||
new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
|
||||
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
|
||||
}
|
||||
|
||||
/** Returns a {@link HttpDataSource.Factory}. */
|
||||
public HttpDataSource.Factory buildHttpDataSourceFactory(
|
||||
TransferListener<? super DataSource> listener) {
|
||||
return new DefaultHttpDataSourceFactory(userAgent, listener);
|
||||
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 DownloadNotificationHelper getDownloadNotificationHelper() {
|
||||
if (downloadNotificationHelper == null) {
|
||||
downloadNotificationHelper =
|
||||
new DownloadNotificationHelper(this, DOWNLOAD_NOTIFICATION_CHANNEL_ID);
|
||||
}
|
||||
return downloadNotificationHelper;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 static com.google.android.exoplayer2.demo.DemoApplication.DOWNLOAD_NOTIFICATION_CHANNEL_ID;
|
||||
|
||||
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 int JOB_ID = 1;
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
||||
|
||||
public DemoDownloadService() {
|
||||
super(
|
||||
FOREGROUND_NOTIFICATION_ID,
|
||||
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
||||
DOWNLOAD_NOTIFICATION_CHANNEL_ID,
|
||||
R.string.exo_download_notification_channel_name,
|
||||
/* channelDescriptionResourceId= */ 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DownloadManager getDownloadManager() {
|
||||
// This will only happen once, because getDownloadManager is guaranteed to be called only once
|
||||
// in the life cycle of the process.
|
||||
DemoApplication application = (DemoApplication) getApplication();
|
||||
DownloadManager downloadManager = application.getDownloadManager();
|
||||
DownloadNotificationHelper downloadNotificationHelper =
|
||||
application.getDownloadNotificationHelper();
|
||||
downloadManager.addListener(
|
||||
new TerminalStateNotificationHelper(
|
||||
this, downloadNotificationHelper, 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 ((DemoApplication) getApplication())
|
||||
.getDownloadNotificationHelper()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.AssetManager;
|
||||
|
|
@ -23,41 +22,63 @@ 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.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;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* An activity for selecting from a list of samples.
|
||||
*/
|
||||
public class SampleChooserActivity extends Activity {
|
||||
/** 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;
|
||||
|
|
@ -80,8 +101,60 @@ public class SampleChooserActivity extends Activity {
|
|||
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) {
|
||||
|
|
@ -89,20 +162,71 @@ public class SampleChooserActivity extends Activity {
|
|||
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
ExpandableListView sampleList = 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;
|
||||
}
|
||||
});
|
||||
sampleAdapter.setSampleGroups(groups);
|
||||
}
|
||||
|
||||
private void onSampleSelected(Sample sample) {
|
||||
startActivity(sample.buildIntent(this));
|
||||
@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>> {
|
||||
|
|
@ -114,7 +238,8 @@ public class SampleChooserActivity extends Activity {
|
|||
List<SampleGroup> result = new ArrayList<>();
|
||||
Context context = getApplicationContext();
|
||||
String userAgent = Util.getUserAgent(context, "ExoPlayerDemo");
|
||||
DataSource dataSource = new DefaultDataSource(context, null, userAgent, false);
|
||||
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);
|
||||
|
|
@ -176,15 +301,21 @@ public class SampleChooserActivity extends Activity {
|
|||
|
||||
private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
|
||||
String sampleName = null;
|
||||
String uri = null;
|
||||
Uri uri = null;
|
||||
String extension = null;
|
||||
UUID drmUuid = null;
|
||||
boolean isLive = false;
|
||||
String drmScheme = null;
|
||||
String drmLicenseUrl = null;
|
||||
String[] drmKeyRequestProperties = null;
|
||||
String[] drmSessionForClearTypes = null;
|
||||
boolean drmMultiSession = false;
|
||||
boolean preferExtensionDecoders = 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()) {
|
||||
|
|
@ -194,25 +325,21 @@ public class SampleChooserActivity extends Activity {
|
|||
sampleName = reader.nextString();
|
||||
break;
|
||||
case "uri":
|
||||
uri = reader.nextString();
|
||||
uri = Uri.parse(reader.nextString());
|
||||
break;
|
||||
case "extension":
|
||||
extension = reader.nextString();
|
||||
break;
|
||||
case "drm_scheme":
|
||||
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
|
||||
String drmScheme = reader.nextString();
|
||||
drmUuid = Util.getDrmUuid(drmScheme);
|
||||
Assertions.checkState(drmUuid != null, "Invalid drm_scheme: " + drmScheme);
|
||||
drmScheme = reader.nextString();
|
||||
break;
|
||||
case "is_live":
|
||||
isLive = reader.nextBoolean();
|
||||
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()) {
|
||||
|
|
@ -222,41 +349,79 @@ public class SampleChooserActivity extends Activity {
|
|||
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 "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));
|
||||
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 = drmUuid == null ? null : new DrmInfo(drmUuid, drmLicenseUrl,
|
||||
drmKeyRequestProperties, drmMultiSession);
|
||||
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[playlistSamples.size()]);
|
||||
return new PlaylistSample(sampleName, preferExtensionDecoders, drmInfo,
|
||||
playlistSamplesArray);
|
||||
UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
|
||||
return new PlaylistSample(sampleName, playlistSamplesArray);
|
||||
} else {
|
||||
return new UriSample(sampleName, preferExtensionDecoders, drmInfo, uri, extension,
|
||||
adTagUri);
|
||||
return new UriSample(
|
||||
sampleName,
|
||||
uri,
|
||||
extension,
|
||||
isLive,
|
||||
drmInfo,
|
||||
adTagUri != null ? Uri.parse(adTagUri) : null,
|
||||
sphericalStereoMode,
|
||||
subtitleInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -273,14 +438,17 @@ public class SampleChooserActivity extends Activity {
|
|||
|
||||
}
|
||||
|
||||
private static final class SampleAdapter extends BaseExpandableListAdapter {
|
||||
private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {
|
||||
|
||||
private final Context context;
|
||||
private final List<SampleGroup> sampleGroups;
|
||||
private List<SampleGroup> sampleGroups;
|
||||
|
||||
public SampleAdapter(Context context, List<SampleGroup> sampleGroups) {
|
||||
this.context = context;
|
||||
public SampleAdapter() {
|
||||
sampleGroups = Collections.emptyList();
|
||||
}
|
||||
|
||||
public void setSampleGroups(List<SampleGroup> sampleGroups) {
|
||||
this.sampleGroups = sampleGroups;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -298,10 +466,12 @@ public class SampleChooserActivity extends Activity {
|
|||
View convertView, ViewGroup parent) {
|
||||
View view = convertView;
|
||||
if (view == null) {
|
||||
view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent,
|
||||
false);
|
||||
view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false);
|
||||
View downloadButton = view.findViewById(R.id.download_button);
|
||||
downloadButton.setOnClickListener(this);
|
||||
downloadButton.setFocusable(false);
|
||||
}
|
||||
((TextView) view).setText(getChild(groupPosition, childPosition).name);
|
||||
initializeChildView(view, getChild(groupPosition, childPosition));
|
||||
return view;
|
||||
}
|
||||
|
||||
|
|
@ -325,8 +495,9 @@ public class SampleChooserActivity extends Activity {
|
|||
ViewGroup parent) {
|
||||
View view = convertView;
|
||||
if (view == null) {
|
||||
view = LayoutInflater.from(context).inflate(android.R.layout.simple_expandable_list_item_1,
|
||||
parent, false);
|
||||
view =
|
||||
getLayoutInflater()
|
||||
.inflate(android.R.layout.simple_expandable_list_item_1, parent, false);
|
||||
}
|
||||
((TextView) view).setText(getGroup(groupPosition).title);
|
||||
return view;
|
||||
|
|
@ -347,6 +518,25 @@ public class SampleChooserActivity extends Activity {
|
|||
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 {
|
||||
|
|
@ -360,102 +550,4 @@ public class SampleChooserActivity extends Activity {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class DrmInfo {
|
||||
public final UUID drmSchemeUuid;
|
||||
public final String drmLicenseUrl;
|
||||
public final String[] drmKeyRequestProperties;
|
||||
public final boolean drmMultiSession;
|
||||
|
||||
public DrmInfo(UUID drmSchemeUuid, String drmLicenseUrl,
|
||||
String[] drmKeyRequestProperties, boolean drmMultiSession) {
|
||||
this.drmSchemeUuid = drmSchemeUuid;
|
||||
this.drmLicenseUrl = drmLicenseUrl;
|
||||
this.drmKeyRequestProperties = drmKeyRequestProperties;
|
||||
this.drmMultiSession = drmMultiSession;
|
||||
}
|
||||
|
||||
public void updateIntent(Intent intent) {
|
||||
Assertions.checkNotNull(intent);
|
||||
intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmSchemeUuid.toString());
|
||||
intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl);
|
||||
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
|
||||
intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class Sample {
|
||||
public final String name;
|
||||
public final boolean preferExtensionDecoders;
|
||||
public final DrmInfo drmInfo;
|
||||
|
||||
public Sample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo) {
|
||||
this.name = name;
|
||||
this.preferExtensionDecoders = preferExtensionDecoders;
|
||||
this.drmInfo = drmInfo;
|
||||
}
|
||||
|
||||
public Intent buildIntent(Context context) {
|
||||
Intent intent = new Intent(context, PlayerActivity.class);
|
||||
intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders);
|
||||
if (drmInfo != null) {
|
||||
drmInfo.updateIntent(intent);
|
||||
}
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class UriSample extends Sample {
|
||||
|
||||
public final String uri;
|
||||
public final String extension;
|
||||
public final String adTagUri;
|
||||
|
||||
public UriSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, String uri,
|
||||
String extension, String adTagUri) {
|
||||
super(name, preferExtensionDecoders, drmInfo);
|
||||
this.uri = uri;
|
||||
this.extension = extension;
|
||||
this.adTagUri = adTagUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent buildIntent(Context context) {
|
||||
return super.buildIntent(context)
|
||||
.setData(Uri.parse(uri))
|
||||
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
||||
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
|
||||
.setAction(PlayerActivity.ACTION_VIEW);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class PlaylistSample extends Sample {
|
||||
|
||||
public final UriSample[] children;
|
||||
|
||||
public PlaylistSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo,
|
||||
UriSample... children) {
|
||||
super(name, preferExtensionDecoders, drmInfo);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# 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);
|
||||
}
|
||||
BIN
demos/main/src/main/res/drawable-hdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 199 B |
BIN
demos/main/src/main/res/drawable-hdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 218 B |
BIN
demos/main/src/main/res/drawable-mdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
demos/main/src/main/res/drawable-mdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 182 B |
BIN
demos/main/src/main/res/drawable-xhdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 187 B |
BIN
demos/main/src/main/res/drawable-xhdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 304 B |
BIN
demos/main/src/main/res/drawable-xxhdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 261 B |
BIN
demos/main/src/main/res/drawable-xxhdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 450 B |
BIN
demos/main/src/main/res/drawable-xxxhdpi/ic_download.png
Normal file
|
After Width: | Height: | Size: 263 B |
BIN
demos/main/src/main/res/drawable-xxxhdpi/ic_download_done.png
Normal file
|
After Width: | Height: | Size: 575 B |
|
|
@ -42,7 +42,15 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"/>
|
||||
android:visibility="gone">
|
||||
|
||||
<Button android:id="@+id/select_tracks_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/track_selection_title"
|
||||
android:enabled="false"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
|
|
|||
38
demos/main/src/main/res/layout/sample_list_item.xml
Normal 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>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||
<?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.
|
||||
|
|
@ -13,13 +13,47 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
<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">
|
||||
|
||||
<LinearLayout android:id="@+id/root"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
<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">
|
||||
|
||||
</ScrollView>
|
||||
<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>
|
||||
|
|
|
|||
30
demos/main/src/main/res/menu/sample_chooser_menu.xml
Normal 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>
|
||||
|
|
@ -17,21 +17,19 @@
|
|||
|
||||
<string name="application_name">ExoPlayer</string>
|
||||
|
||||
<string name="video">Video</string>
|
||||
|
||||
<string name="audio">Audio</string>
|
||||
|
||||
<string name="text">Text</string>
|
||||
|
||||
<string name="selection_disabled">Disabled</string>
|
||||
|
||||
<string name="selection_default">Default</string>
|
||||
<string name="track_selection_title">Select tracks</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>
|
||||
|
||||
|
|
@ -55,4 +53,24 @@
|
|||
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,16 @@
|
|||
-->
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="PlayerTheme" parent="android:Theme.Holo">
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<style name="TrackSelectionDialogThemeOverlay" parent="ThemeOverlay.AppCompat.Dialog.Alert">
|
||||
<item name="windowNoTitle">false</item>
|
||||
</style>
|
||||
|
||||
<style name="PlayerTheme" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="PlayerTheme.Spherical">
|
||||
<item name="surface_type">spherical_gl_surface_view</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
21
demos/surface/README.md
Normal 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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2017 The Android Open Source Project
|
||||
// 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.
|
||||
|
|
@ -16,13 +16,17 @@ apply plugin: 'com.android.application'
|
|||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion 16
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
minSdkVersion 29
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -31,13 +35,10 @@ android {
|
|||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// The demo app does not have translations.
|
||||
// This demo app does not have translations.
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
|
@ -46,8 +47,5 @@ dependencies {
|
|||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'extension-ima')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
<!-- 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.
|
||||
|
|
@ -14,26 +14,28 @@
|
|||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.imademo"
|
||||
android:versionCode="2700"
|
||||
android:versionName="2.7.0">
|
||||
|
||||
package="com.google.android.exoplayer2.surfacedemo">
|
||||
<uses-sdk/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/>
|
||||
|
||||
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true" android:allowBackup="false">
|
||||
|
||||
<activity android:name="com.google.android.exoplayer2.imademo.MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/PlayerTheme">
|
||||
<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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
51
demos/surface/src/main/res/layout/main_activity.xml
Normal 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>
|
||||
|
||||
BIN
demos/surface/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demos/surface/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
demos/surface/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
demos/surface/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
demos/surface/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
<?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.
|
||||
|
|
@ -15,7 +14,10 @@
|
|||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="exo_media_action_repeat_all_description">"Korda kõike"</string>
|
||||
<string name="exo_media_action_repeat_off_description">"Ära korda midagi"</string>
|
||||
<string name="exo_media_action_repeat_one_description">"Korda ühte"</string>
|
||||
|
||||
<string name="application_name">ExoPlayer SurfaceControl demo</string>
|
||||
<string name="no_output_label">No output</string>
|
||||
<string name="full_screen_label">Full screen</string>
|
||||
<string name="new_activity_label">New activity</string>
|
||||
|
||||
</resources>
|
||||
134
extensions/av1/README.md
Normal 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
|
||||
73
extensions/av1/build.gradle
Normal 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'
|
||||
7
extensions/av1/proguard-rules.txt
Normal 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>;
|
||||
}
|
||||
|
||||
17
extensions/av1/src/main/AndroidManifest.xml
Normal 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"/>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.ext.av1;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
||||
62
extensions/av1/src/main/jni/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# libgav1JNI requires modern CMake.
|
||||
cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
|
||||
|
||||
# libgav1JNI requires C++11.
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
project(libgav1JNI C CXX)
|
||||
|
||||
# Devices using armeabi-v7a are not required to support
|
||||
# Neon which is why Neon is disabled by default for
|
||||
# armeabi-v7a build. This flag enables it.
|
||||
if(${ANDROID_ABI} MATCHES "armeabi-v7a")
|
||||
add_compile_options("-mfpu=neon")
|
||||
add_compile_options("-marm")
|
||||
add_compile_options("-fPIC")
|
||||
endif()
|
||||
|
||||
string(TOLOWER "${CMAKE_BUILD_TYPE}" build_type)
|
||||
if(build_type MATCHES "^rel")
|
||||
add_compile_options("-O2")
|
||||
endif()
|
||||
|
||||
set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(libgav1_jni_build "${CMAKE_BINARY_DIR}")
|
||||
set(libgav1_jni_output_directory
|
||||
${libgav1_jni_root}/../libs/${ANDROID_ABI}/)
|
||||
|
||||
set(libgav1_root "${libgav1_jni_root}/libgav1")
|
||||
set(libgav1_build "${libgav1_jni_build}/libgav1")
|
||||
|
||||
set(cpu_features_root "${libgav1_jni_root}/cpu_features")
|
||||
set(cpu_features_build "${libgav1_jni_build}/cpu_features")
|
||||
|
||||
# Build cpu_features library.
|
||||
add_subdirectory("${cpu_features_root}"
|
||||
"${cpu_features_build}"
|
||||
EXCLUDE_FROM_ALL)
|
||||
|
||||
# Build libgav1.
|
||||
add_subdirectory("${libgav1_root}"
|
||||
"${libgav1_build}"
|
||||
EXCLUDE_FROM_ALL)
|
||||
|
||||
# Build libgav1JNI.
|
||||
add_library(gav1JNI
|
||||
SHARED
|
||||
gav1_jni.cc)
|
||||
|
||||
# Locate NDK log library.
|
||||
find_library(android_log_lib log)
|
||||
|
||||
# Link libgav1JNI against used libraries.
|
||||
target_link_libraries(gav1JNI
|
||||
PRIVATE android
|
||||
PRIVATE cpu_features
|
||||
PRIVATE libgav1_static
|
||||
PRIVATE ${android_log_lib})
|
||||
|
||||
# Specify output directory for libgav1JNI.
|
||||
set_target_properties(gav1JNI PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY
|
||||
${libgav1_jni_output_directory})
|
||||
862
extensions/av1/src/main/jni/gav1_jni.cc
Normal file
|
|
@ -0,0 +1,862 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <android/log.h>
|
||||
#include <android/native_window.h>
|
||||
#include <android/native_window_jni.h>
|
||||
|
||||
#include "cpu_features_macros.h" // NOLINT
|
||||
#ifdef CPU_FEATURES_ARCH_ARM
|
||||
#include "cpuinfo_arm.h" // NOLINT
|
||||
#endif // CPU_FEATURES_ARCH_ARM
|
||||
#ifdef CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
#include <arm_neon.h>
|
||||
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
#include <jni.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex> // NOLINT
|
||||
#include <new>
|
||||
|
||||
#include "gav1/decoder.h"
|
||||
|
||||
#define LOG_TAG "gav1_jni"
|
||||
#define LOGE(...) \
|
||||
((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
|
||||
|
||||
#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \
|
||||
extern "C" { \
|
||||
JNIEXPORT RETURN_TYPE \
|
||||
Java_com_google_android_exoplayer2_ext_av1_Gav1Decoder_##NAME( \
|
||||
JNIEnv* env, jobject thiz, ##__VA_ARGS__); \
|
||||
} \
|
||||
JNIEXPORT RETURN_TYPE \
|
||||
Java_com_google_android_exoplayer2_ext_av1_Gav1Decoder_##NAME( \
|
||||
JNIEnv* env, jobject thiz, ##__VA_ARGS__)
|
||||
|
||||
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
JNIEnv* env;
|
||||
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
|
||||
return -1;
|
||||
}
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// YUV plane indices.
|
||||
const int kPlaneY = 0;
|
||||
const int kPlaneU = 1;
|
||||
const int kPlaneV = 2;
|
||||
const int kMaxPlanes = 3;
|
||||
|
||||
// Android YUV format. See:
|
||||
// https://developer.android.com/reference/android/graphics/ImageFormat.html#YV12.
|
||||
const int kImageFormatYV12 = 0x32315659;
|
||||
|
||||
// LINT.IfChange
|
||||
// Output modes.
|
||||
const int kOutputModeYuv = 0;
|
||||
const int kOutputModeSurfaceYuv = 1;
|
||||
// LINT.ThenChange(../../../../../library/common/src/main/java/com/google/android/exoplayer2/C.java)
|
||||
|
||||
// LINT.IfChange
|
||||
const int kColorSpaceUnknown = 0;
|
||||
// LINT.ThenChange(../../../../../library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java)
|
||||
|
||||
// LINT.IfChange
|
||||
// Return codes for jni methods.
|
||||
const int kStatusError = 0;
|
||||
const int kStatusOk = 1;
|
||||
const int kStatusDecodeOnly = 2;
|
||||
// LINT.ThenChange(../java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java)
|
||||
|
||||
// Status codes specific to the JNI wrapper code.
|
||||
enum JniStatusCode {
|
||||
kJniStatusOk = 0,
|
||||
kJniStatusOutOfMemory = -1,
|
||||
kJniStatusBufferAlreadyReleased = -2,
|
||||
kJniStatusInvalidNumOfPlanes = -3,
|
||||
kJniStatusBitDepth12NotSupportedWithYuv = -4,
|
||||
kJniStatusHighBitDepthNotSupportedWithSurfaceYuv = -5,
|
||||
kJniStatusANativeWindowError = -6,
|
||||
kJniStatusBufferResizeError = -7,
|
||||
kJniStatusNeonNotSupported = -8
|
||||
};
|
||||
|
||||
const char* GetJniErrorMessage(JniStatusCode error_code) {
|
||||
switch (error_code) {
|
||||
case kJniStatusOutOfMemory:
|
||||
return "Out of memory.";
|
||||
case kJniStatusBufferAlreadyReleased:
|
||||
return "JNI buffer already released.";
|
||||
case kJniStatusBitDepth12NotSupportedWithYuv:
|
||||
return "Bit depth 12 is not supported with YUV.";
|
||||
case kJniStatusHighBitDepthNotSupportedWithSurfaceYuv:
|
||||
return "High bit depth (10 or 12 bits per pixel) output format is not "
|
||||
"supported with YUV surface.";
|
||||
case kJniStatusInvalidNumOfPlanes:
|
||||
return "Libgav1 decoded buffer has invalid number of planes.";
|
||||
case kJniStatusANativeWindowError:
|
||||
return "ANativeWindow error.";
|
||||
case kJniStatusBufferResizeError:
|
||||
return "Buffer resize failed.";
|
||||
case kJniStatusNeonNotSupported:
|
||||
return "Neon is not supported.";
|
||||
default:
|
||||
return "Unrecognized error code.";
|
||||
}
|
||||
}
|
||||
|
||||
// Manages frame buffer and reference information.
|
||||
class JniFrameBuffer {
|
||||
public:
|
||||
explicit JniFrameBuffer(int id) : id_(id), reference_count_(0) {}
|
||||
~JniFrameBuffer() {
|
||||
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
|
||||
delete[] raw_buffer_[plane_index];
|
||||
}
|
||||
}
|
||||
|
||||
void SetFrameData(const libgav1::DecoderBuffer& decoder_buffer) {
|
||||
for (int plane_index = kPlaneY; plane_index < decoder_buffer.NumPlanes();
|
||||
plane_index++) {
|
||||
stride_[plane_index] = decoder_buffer.stride[plane_index];
|
||||
plane_[plane_index] = decoder_buffer.plane[plane_index];
|
||||
displayed_width_[plane_index] =
|
||||
decoder_buffer.displayed_width[plane_index];
|
||||
displayed_height_[plane_index] =
|
||||
decoder_buffer.displayed_height[plane_index];
|
||||
}
|
||||
}
|
||||
|
||||
int Stride(int plane_index) const { return stride_[plane_index]; }
|
||||
uint8_t* Plane(int plane_index) const { return plane_[plane_index]; }
|
||||
int DisplayedWidth(int plane_index) const {
|
||||
return displayed_width_[plane_index];
|
||||
}
|
||||
int DisplayedHeight(int plane_index) const {
|
||||
return displayed_height_[plane_index];
|
||||
}
|
||||
|
||||
// Methods maintaining reference count are not thread-safe. They must be
|
||||
// called with a lock held.
|
||||
void AddReference() { ++reference_count_; }
|
||||
void RemoveReference() { reference_count_--; }
|
||||
bool InUse() const { return reference_count_ != 0; }
|
||||
|
||||
uint8_t* RawBuffer(int plane_index) const { return raw_buffer_[plane_index]; }
|
||||
int Id() const { return id_; }
|
||||
|
||||
// Attempts to reallocate data planes if the existing ones don't have enough
|
||||
// capacity. Returns true if the allocation was successful or wasn't needed,
|
||||
// false if the allocation failed.
|
||||
bool MaybeReallocateGav1DataPlanes(int y_plane_min_size,
|
||||
int uv_plane_min_size) {
|
||||
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
|
||||
const int min_size =
|
||||
(plane_index == kPlaneY) ? y_plane_min_size : uv_plane_min_size;
|
||||
if (raw_buffer_size_[plane_index] >= min_size) continue;
|
||||
delete[] raw_buffer_[plane_index];
|
||||
raw_buffer_[plane_index] = new (std::nothrow) uint8_t[min_size];
|
||||
if (!raw_buffer_[plane_index]) {
|
||||
raw_buffer_size_[plane_index] = 0;
|
||||
return false;
|
||||
}
|
||||
raw_buffer_size_[plane_index] = min_size;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
int stride_[kMaxPlanes];
|
||||
uint8_t* plane_[kMaxPlanes];
|
||||
int displayed_width_[kMaxPlanes];
|
||||
int displayed_height_[kMaxPlanes];
|
||||
const int id_;
|
||||
int reference_count_;
|
||||
// Pointers to the raw buffers allocated for the data planes.
|
||||
uint8_t* raw_buffer_[kMaxPlanes] = {};
|
||||
// Sizes of the raw buffers in bytes.
|
||||
size_t raw_buffer_size_[kMaxPlanes] = {};
|
||||
};
|
||||
|
||||
// Manages frame buffers used by libgav1 decoder and ExoPlayer.
|
||||
// Handles synchronization between libgav1 and ExoPlayer threads.
|
||||
class JniBufferManager {
|
||||
public:
|
||||
~JniBufferManager() {
|
||||
// This lock does not do anything since libgav1 has released all the frame
|
||||
// buffers. It exists to merely be consistent with all other usage of
|
||||
// |all_buffers_| and |all_buffer_count_|.
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
while (all_buffer_count_--) {
|
||||
delete all_buffers_[all_buffer_count_];
|
||||
}
|
||||
}
|
||||
|
||||
JniStatusCode GetBuffer(size_t y_plane_min_size, size_t uv_plane_min_size,
|
||||
JniFrameBuffer** jni_buffer) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
JniFrameBuffer* output_buffer;
|
||||
if (free_buffer_count_) {
|
||||
output_buffer = free_buffers_[--free_buffer_count_];
|
||||
} else if (all_buffer_count_ < kMaxFrames) {
|
||||
output_buffer = new (std::nothrow) JniFrameBuffer(all_buffer_count_);
|
||||
if (output_buffer == nullptr) return kJniStatusOutOfMemory;
|
||||
all_buffers_[all_buffer_count_++] = output_buffer;
|
||||
} else {
|
||||
// Maximum number of buffers is being used.
|
||||
return kJniStatusOutOfMemory;
|
||||
}
|
||||
if (!output_buffer->MaybeReallocateGav1DataPlanes(y_plane_min_size,
|
||||
uv_plane_min_size)) {
|
||||
return kJniStatusOutOfMemory;
|
||||
}
|
||||
|
||||
output_buffer->AddReference();
|
||||
*jni_buffer = output_buffer;
|
||||
|
||||
return kJniStatusOk;
|
||||
}
|
||||
|
||||
JniFrameBuffer* GetBuffer(int id) const { return all_buffers_[id]; }
|
||||
|
||||
void AddBufferReference(int id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
all_buffers_[id]->AddReference();
|
||||
}
|
||||
|
||||
JniStatusCode ReleaseBuffer(int id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
JniFrameBuffer* buffer = all_buffers_[id];
|
||||
if (!buffer->InUse()) {
|
||||
return kJniStatusBufferAlreadyReleased;
|
||||
}
|
||||
buffer->RemoveReference();
|
||||
if (!buffer->InUse()) {
|
||||
free_buffers_[free_buffer_count_++] = buffer;
|
||||
}
|
||||
return kJniStatusOk;
|
||||
}
|
||||
|
||||
private:
|
||||
static const int kMaxFrames = 32;
|
||||
|
||||
JniFrameBuffer* all_buffers_[kMaxFrames];
|
||||
int all_buffer_count_ = 0;
|
||||
|
||||
JniFrameBuffer* free_buffers_[kMaxFrames];
|
||||
int free_buffer_count_ = 0;
|
||||
|
||||
std::mutex mutex_;
|
||||
};
|
||||
|
||||
struct JniContext {
|
||||
~JniContext() {
|
||||
if (native_window) {
|
||||
ANativeWindow_release(native_window);
|
||||
}
|
||||
}
|
||||
|
||||
bool MaybeAcquireNativeWindow(JNIEnv* env, jobject new_surface) {
|
||||
if (surface == new_surface) {
|
||||
return true;
|
||||
}
|
||||
if (native_window) {
|
||||
ANativeWindow_release(native_window);
|
||||
}
|
||||
native_window_width = 0;
|
||||
native_window_height = 0;
|
||||
native_window = ANativeWindow_fromSurface(env, new_surface);
|
||||
if (native_window == nullptr) {
|
||||
jni_status_code = kJniStatusANativeWindowError;
|
||||
surface = nullptr;
|
||||
return false;
|
||||
}
|
||||
surface = new_surface;
|
||||
return true;
|
||||
}
|
||||
|
||||
jfieldID decoder_private_field;
|
||||
jfieldID output_mode_field;
|
||||
jfieldID data_field;
|
||||
jmethodID init_for_private_frame_method;
|
||||
jmethodID init_for_yuv_frame_method;
|
||||
|
||||
JniBufferManager buffer_manager;
|
||||
// The libgav1 decoder instance has to be deleted before |buffer_manager| is
|
||||
// destructed. This will make sure that libgav1 releases all the frame
|
||||
// buffers that it might be holding references to. So this has to be declared
|
||||
// after |buffer_manager| since the destruction happens in reverse order of
|
||||
// declaration.
|
||||
libgav1::Decoder decoder;
|
||||
|
||||
ANativeWindow* native_window = nullptr;
|
||||
jobject surface = nullptr;
|
||||
int native_window_width = 0;
|
||||
int native_window_height = 0;
|
||||
|
||||
Libgav1StatusCode libgav1_status_code = kLibgav1StatusOk;
|
||||
JniStatusCode jni_status_code = kJniStatusOk;
|
||||
};
|
||||
|
||||
// Aligns |value| to the desired |alignment|. |alignment| must be a power of 2.
|
||||
template <typename T>
|
||||
constexpr T Align(T value, T alignment) {
|
||||
const T alignment_mask = alignment - 1;
|
||||
return (value + alignment_mask) & ~alignment_mask;
|
||||
}
|
||||
|
||||
// Aligns |addr| to the desired |alignment|. |alignment| must be a power of 2.
|
||||
uint8_t* AlignAddr(uint8_t* const addr, const size_t alignment) {
|
||||
const auto value = reinterpret_cast<size_t>(addr);
|
||||
return reinterpret_cast<uint8_t*>(Align(value, alignment));
|
||||
}
|
||||
|
||||
// Libgav1 frame buffer callbacks return 0 on success, -1 on failure.
|
||||
|
||||
int Libgav1OnFrameBufferSizeChanged(void* /*callback_private_data*/,
|
||||
int /*bitdepth*/,
|
||||
libgav1::ImageFormat /*image_format*/,
|
||||
int /*width*/, int /*height*/,
|
||||
int /*left_border*/, int /*right_border*/,
|
||||
int /*top_border*/, int /*bottom_border*/,
|
||||
int /*stride_alignment*/) {
|
||||
// The libgav1 decoder calls this callback to provide information on the
|
||||
// subsequent frames in the video. JniBufferManager ignores this information.
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Libgav1GetFrameBuffer(void* callback_private_data, int bitdepth,
|
||||
libgav1::ImageFormat image_format, int width,
|
||||
int height, int left_border, int right_border,
|
||||
int top_border, int bottom_border,
|
||||
int stride_alignment,
|
||||
libgav1::FrameBuffer2* frame_buffer) {
|
||||
bool is_monochrome = false;
|
||||
int8_t subsampling_x = 1;
|
||||
int8_t subsampling_y = 1;
|
||||
switch (image_format) {
|
||||
case libgav1::kImageFormatYuv420:
|
||||
break;
|
||||
case libgav1::kImageFormatYuv422:
|
||||
subsampling_y = 0;
|
||||
break;
|
||||
case libgav1::kImageFormatYuv444:
|
||||
subsampling_x = subsampling_y = 0;
|
||||
break;
|
||||
default:
|
||||
// image_format is libgav1::kImageFormatMonochrome400. (AV1 has only four
|
||||
// image formats, hardcoded in the spec).
|
||||
is_monochrome = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate y_stride (in bytes). It is padded to a multiple of
|
||||
// |stride_alignment| bytes.
|
||||
int y_stride = width + left_border + right_border;
|
||||
if (bitdepth > 8) y_stride *= sizeof(uint16_t);
|
||||
y_stride = Align(y_stride, stride_alignment);
|
||||
// Size of the Y plane in bytes.
|
||||
const uint64_t y_plane_size =
|
||||
(height + top_border + bottom_border) * static_cast<uint64_t>(y_stride) +
|
||||
(stride_alignment - 1);
|
||||
|
||||
const int uv_width = is_monochrome ? 0 : width >> subsampling_x;
|
||||
const int uv_height = is_monochrome ? 0 : height >> subsampling_y;
|
||||
const int uv_left_border = is_monochrome ? 0 : left_border >> subsampling_x;
|
||||
const int uv_right_border = is_monochrome ? 0 : right_border >> subsampling_x;
|
||||
const int uv_top_border = is_monochrome ? 0 : top_border >> subsampling_y;
|
||||
const int uv_bottom_border =
|
||||
is_monochrome ? 0 : bottom_border >> subsampling_y;
|
||||
|
||||
// Calculate uv_stride (in bytes). It is padded to a multiple of
|
||||
// |stride_alignment| bytes.
|
||||
int uv_stride = uv_width + uv_left_border + uv_right_border;
|
||||
if (bitdepth > 8) uv_stride *= sizeof(uint16_t);
|
||||
uv_stride = Align(uv_stride, stride_alignment);
|
||||
// Size of the U or V plane in bytes.
|
||||
const uint64_t uv_plane_size =
|
||||
is_monochrome ? 0
|
||||
: (uv_height + uv_top_border + uv_bottom_border) *
|
||||
static_cast<uint64_t>(uv_stride) +
|
||||
(stride_alignment - 1);
|
||||
|
||||
// Check if it is safe to cast y_plane_size and uv_plane_size to size_t.
|
||||
if (y_plane_size > SIZE_MAX || uv_plane_size > SIZE_MAX) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
JniContext* const context = static_cast<JniContext*>(callback_private_data);
|
||||
JniFrameBuffer* jni_buffer;
|
||||
context->jni_status_code = context->buffer_manager.GetBuffer(
|
||||
static_cast<size_t>(y_plane_size), static_cast<size_t>(uv_plane_size),
|
||||
&jni_buffer);
|
||||
if (context->jni_status_code != kJniStatusOk) {
|
||||
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t* const y_buffer = jni_buffer->RawBuffer(0);
|
||||
uint8_t* const u_buffer = !is_monochrome ? jni_buffer->RawBuffer(1) : nullptr;
|
||||
uint8_t* const v_buffer = !is_monochrome ? jni_buffer->RawBuffer(2) : nullptr;
|
||||
|
||||
int left_border_bytes = left_border;
|
||||
int uv_left_border_bytes = uv_left_border;
|
||||
if (bitdepth > 8) {
|
||||
left_border_bytes *= sizeof(uint16_t);
|
||||
uv_left_border_bytes *= sizeof(uint16_t);
|
||||
}
|
||||
frame_buffer->plane[0] = AlignAddr(
|
||||
y_buffer + (top_border * y_stride) + left_border_bytes, stride_alignment);
|
||||
frame_buffer->plane[1] =
|
||||
AlignAddr(u_buffer + (uv_top_border * uv_stride) + uv_left_border_bytes,
|
||||
stride_alignment);
|
||||
frame_buffer->plane[2] =
|
||||
AlignAddr(v_buffer + (uv_top_border * uv_stride) + uv_left_border_bytes,
|
||||
stride_alignment);
|
||||
|
||||
frame_buffer->stride[0] = y_stride;
|
||||
frame_buffer->stride[1] = frame_buffer->stride[2] = uv_stride;
|
||||
|
||||
frame_buffer->private_data = reinterpret_cast<void*>(jni_buffer->Id());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Libgav1ReleaseFrameBuffer(void* callback_private_data,
|
||||
void* buffer_private_data) {
|
||||
JniContext* const context = static_cast<JniContext*>(callback_private_data);
|
||||
const int buffer_id = reinterpret_cast<int>(buffer_private_data);
|
||||
context->jni_status_code = context->buffer_manager.ReleaseBuffer(buffer_id);
|
||||
if (context->jni_status_code != kJniStatusOk) {
|
||||
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
|
||||
}
|
||||
}
|
||||
|
||||
void CopyPlane(const uint8_t* source, int source_stride, uint8_t* destination,
|
||||
int destination_stride, int width, int height) {
|
||||
while (height--) {
|
||||
std::memcpy(destination, source, width);
|
||||
source += source_stride;
|
||||
destination += destination_stride;
|
||||
}
|
||||
}
|
||||
|
||||
void CopyFrameToDataBuffer(const libgav1::DecoderBuffer* decoder_buffer,
|
||||
jbyte* data) {
|
||||
for (int plane_index = kPlaneY; plane_index < decoder_buffer->NumPlanes();
|
||||
plane_index++) {
|
||||
const uint64_t length = decoder_buffer->stride[plane_index] *
|
||||
decoder_buffer->displayed_height[plane_index];
|
||||
memcpy(data, decoder_buffer->plane[plane_index], length);
|
||||
data += length;
|
||||
}
|
||||
}
|
||||
|
||||
void Convert10BitFrameTo8BitDataBuffer(
|
||||
const libgav1::DecoderBuffer* decoder_buffer, jbyte* data) {
|
||||
for (int plane_index = kPlaneY; plane_index < decoder_buffer->NumPlanes();
|
||||
plane_index++) {
|
||||
int sample = 0;
|
||||
const uint8_t* source = decoder_buffer->plane[plane_index];
|
||||
for (int i = 0; i < decoder_buffer->displayed_height[plane_index]; i++) {
|
||||
const uint16_t* source_16 = reinterpret_cast<const uint16_t*>(source);
|
||||
for (int j = 0; j < decoder_buffer->displayed_width[plane_index]; j++) {
|
||||
// Lightweight dither. Carryover the remainder of each 10->8 bit
|
||||
// conversion to the next pixel.
|
||||
sample += source_16[j];
|
||||
data[j] = sample >> 2;
|
||||
sample &= 3; // Remainder.
|
||||
}
|
||||
source += decoder_buffer->stride[plane_index];
|
||||
data += decoder_buffer->stride[plane_index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
void Convert10BitFrameTo8BitDataBufferNeon(
|
||||
const libgav1::DecoderBuffer* decoder_buffer, jbyte* data) {
|
||||
uint32x2_t lcg_value = vdup_n_u32(random());
|
||||
lcg_value = vset_lane_u32(random(), lcg_value, 1);
|
||||
// LCG values recommended in "Numerical Recipes".
|
||||
const uint32x2_t LCG_MULT = vdup_n_u32(1664525);
|
||||
const uint32x2_t LCG_INCR = vdup_n_u32(1013904223);
|
||||
|
||||
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
|
||||
const uint8_t* source = decoder_buffer->plane[plane_index];
|
||||
|
||||
for (int i = 0; i < decoder_buffer->displayed_height[plane_index]; i++) {
|
||||
const uint16_t* source_16 = reinterpret_cast<const uint16_t*>(source);
|
||||
uint8_t* destination = reinterpret_cast<uint8_t*>(data);
|
||||
|
||||
// Each read consumes 4 2-byte samples, but to reduce branches and
|
||||
// random steps we unroll to 4 rounds, so each loop consumes 16
|
||||
// samples.
|
||||
const int j_max = decoder_buffer->displayed_width[plane_index] & ~15;
|
||||
int j;
|
||||
for (j = 0; j < j_max; j += 16) {
|
||||
// Run a round of the RNG.
|
||||
lcg_value = vmla_u32(LCG_INCR, lcg_value, LCG_MULT);
|
||||
|
||||
// Round 1.
|
||||
// The lower two bits of this LCG parameterization are garbage,
|
||||
// leaving streaks on the image. We access the upper bits of each
|
||||
// 16-bit lane by shifting. (We use this both as an 8- and 16-bit
|
||||
// vector, so the choice of which one to keep it as is arbitrary.)
|
||||
uint8x8_t randvec =
|
||||
vreinterpret_u8_u16(vshr_n_u16(vreinterpret_u16_u32(lcg_value), 8));
|
||||
|
||||
// We retrieve the values and shift them so that the bits we'll
|
||||
// shift out (after biasing) are in the upper 8 bits of each 16-bit
|
||||
// lane.
|
||||
uint16x4_t values = vshl_n_u16(vld1_u16(source_16), 6);
|
||||
// We add the bias bits in the lower 8 to the shifted values to get
|
||||
// the final values in the upper 8 bits.
|
||||
uint16x4_t added_1 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
|
||||
source_16 += 4;
|
||||
|
||||
// Round 2.
|
||||
// Shifting the randvec bits left by 2 bits, as an 8-bit vector,
|
||||
// should leave us with enough bias to get the needed rounding
|
||||
// operation.
|
||||
randvec = vshl_n_u8(randvec, 2);
|
||||
|
||||
// Retrieve and sum the next 4 pixels.
|
||||
values = vshl_n_u16(vld1_u16(source_16), 6);
|
||||
uint16x4_t added_2 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
|
||||
source_16 += 4;
|
||||
|
||||
// Reinterpret the two added vectors as 8x8, zip them together, and
|
||||
// discard the lower portions.
|
||||
uint8x8_t zipped =
|
||||
vuzp_u8(vreinterpret_u8_u16(added_1), vreinterpret_u8_u16(added_2))
|
||||
.val[1];
|
||||
vst1_u8(destination, zipped);
|
||||
destination += 8;
|
||||
|
||||
// Run it again with the next two rounds using the remaining
|
||||
// entropy in randvec.
|
||||
|
||||
// Round 3.
|
||||
randvec = vshl_n_u8(randvec, 2);
|
||||
values = vshl_n_u16(vld1_u16(source_16), 6);
|
||||
added_1 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
|
||||
source_16 += 4;
|
||||
|
||||
// Round 4.
|
||||
randvec = vshl_n_u8(randvec, 2);
|
||||
values = vshl_n_u16(vld1_u16(source_16), 6);
|
||||
added_2 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
|
||||
source_16 += 4;
|
||||
|
||||
zipped =
|
||||
vuzp_u8(vreinterpret_u8_u16(added_1), vreinterpret_u8_u16(added_2))
|
||||
.val[1];
|
||||
vst1_u8(destination, zipped);
|
||||
destination += 8;
|
||||
}
|
||||
|
||||
uint32_t randval = 0;
|
||||
// For the remaining pixels in each row - usually none, as most
|
||||
// standard sizes are divisible by 32 - convert them "by hand".
|
||||
for (; j < decoder_buffer->displayed_width[plane_index]; j++) {
|
||||
if (!randval) randval = random();
|
||||
destination[j] = (source_16[j] + (randval & 3)) >> 2;
|
||||
randval >>= 2;
|
||||
}
|
||||
|
||||
source += decoder_buffer->stride[plane_index];
|
||||
data += decoder_buffer->stride[plane_index];
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
|
||||
} // namespace
|
||||
|
||||
DECODER_FUNC(jlong, gav1Init, jint threads) {
|
||||
JniContext* context = new (std::nothrow) JniContext();
|
||||
if (context == nullptr) {
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
#ifdef CPU_FEATURES_ARCH_ARM
|
||||
// Libgav1 requires NEON with arm ABIs.
|
||||
#ifdef CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
const cpu_features::ArmFeatures arm_features =
|
||||
cpu_features::GetArmInfo().features;
|
||||
if (!arm_features.neon) {
|
||||
context->jni_status_code = kJniStatusNeonNotSupported;
|
||||
return reinterpret_cast<jlong>(context);
|
||||
}
|
||||
#else
|
||||
context->jni_status_code = kJniStatusNeonNotSupported;
|
||||
return reinterpret_cast<jlong>(context);
|
||||
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
#endif // CPU_FEATURES_ARCH_ARM
|
||||
|
||||
libgav1::DecoderSettings settings;
|
||||
settings.threads = threads;
|
||||
settings.on_frame_buffer_size_changed = Libgav1OnFrameBufferSizeChanged;
|
||||
settings.get_frame_buffer = Libgav1GetFrameBuffer;
|
||||
settings.release_frame_buffer = Libgav1ReleaseFrameBuffer;
|
||||
settings.callback_private_data = context;
|
||||
|
||||
context->libgav1_status_code = context->decoder.Init(&settings);
|
||||
if (context->libgav1_status_code != kLibgav1StatusOk) {
|
||||
return reinterpret_cast<jlong>(context);
|
||||
}
|
||||
|
||||
// Populate JNI References.
|
||||
const jclass outputBufferClass = env->FindClass(
|
||||
"com/google/android/exoplayer2/video/VideoDecoderOutputBuffer");
|
||||
context->decoder_private_field =
|
||||
env->GetFieldID(outputBufferClass, "decoderPrivate", "I");
|
||||
context->output_mode_field = env->GetFieldID(outputBufferClass, "mode", "I");
|
||||
context->data_field =
|
||||
env->GetFieldID(outputBufferClass, "data", "Ljava/nio/ByteBuffer;");
|
||||
context->init_for_private_frame_method =
|
||||
env->GetMethodID(outputBufferClass, "initForPrivateFrame", "(II)V");
|
||||
context->init_for_yuv_frame_method =
|
||||
env->GetMethodID(outputBufferClass, "initForYuvFrame", "(IIIII)Z");
|
||||
|
||||
return reinterpret_cast<jlong>(context);
|
||||
}
|
||||
|
||||
DECODER_FUNC(void, gav1Close, jlong jContext) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
delete context;
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, gav1Decode, jlong jContext, jobject encodedData,
|
||||
jint length) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
const uint8_t* const buffer = reinterpret_cast<const uint8_t*>(
|
||||
env->GetDirectBufferAddress(encodedData));
|
||||
context->libgav1_status_code =
|
||||
context->decoder.EnqueueFrame(buffer, length, /*user_private_data=*/0);
|
||||
if (context->libgav1_status_code != kLibgav1StatusOk) {
|
||||
return kStatusError;
|
||||
}
|
||||
return kStatusOk;
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, gav1GetFrame, jlong jContext, jobject jOutputBuffer,
|
||||
jboolean decodeOnly) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
const libgav1::DecoderBuffer* decoder_buffer;
|
||||
context->libgav1_status_code = context->decoder.DequeueFrame(&decoder_buffer);
|
||||
if (context->libgav1_status_code != kLibgav1StatusOk) {
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
if (decodeOnly || decoder_buffer == nullptr) {
|
||||
// This is not an error. The input data was decode-only or no displayable
|
||||
// frames are available.
|
||||
return kStatusDecodeOnly;
|
||||
}
|
||||
|
||||
const int output_mode =
|
||||
env->GetIntField(jOutputBuffer, context->output_mode_field);
|
||||
if (output_mode == kOutputModeYuv) {
|
||||
// Resize the buffer if required. Default color conversion will be used as
|
||||
// libgav1::DecoderBuffer doesn't expose color space info.
|
||||
const jboolean init_result = env->CallBooleanMethod(
|
||||
jOutputBuffer, context->init_for_yuv_frame_method,
|
||||
decoder_buffer->displayed_width[kPlaneY],
|
||||
decoder_buffer->displayed_height[kPlaneY],
|
||||
decoder_buffer->stride[kPlaneY], decoder_buffer->stride[kPlaneU],
|
||||
kColorSpaceUnknown);
|
||||
if (env->ExceptionCheck()) {
|
||||
// Exception is thrown in Java when returning from the native call.
|
||||
return kStatusError;
|
||||
}
|
||||
if (!init_result) {
|
||||
context->jni_status_code = kJniStatusBufferResizeError;
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
const jobject data_object =
|
||||
env->GetObjectField(jOutputBuffer, context->data_field);
|
||||
jbyte* const data =
|
||||
reinterpret_cast<jbyte*>(env->GetDirectBufferAddress(data_object));
|
||||
|
||||
switch (decoder_buffer->bitdepth) {
|
||||
case 8:
|
||||
CopyFrameToDataBuffer(decoder_buffer, data);
|
||||
break;
|
||||
case 10:
|
||||
#ifdef CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
Convert10BitFrameTo8BitDataBufferNeon(decoder_buffer, data);
|
||||
#else
|
||||
Convert10BitFrameTo8BitDataBuffer(decoder_buffer, data);
|
||||
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||
break;
|
||||
default:
|
||||
context->jni_status_code = kJniStatusBitDepth12NotSupportedWithYuv;
|
||||
return kStatusError;
|
||||
}
|
||||
} else if (output_mode == kOutputModeSurfaceYuv) {
|
||||
if (decoder_buffer->bitdepth != 8) {
|
||||
context->jni_status_code =
|
||||
kJniStatusHighBitDepthNotSupportedWithSurfaceYuv;
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
if (decoder_buffer->NumPlanes() > kMaxPlanes) {
|
||||
context->jni_status_code = kJniStatusInvalidNumOfPlanes;
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
const int buffer_id =
|
||||
reinterpret_cast<int>(decoder_buffer->buffer_private_data);
|
||||
context->buffer_manager.AddBufferReference(buffer_id);
|
||||
JniFrameBuffer* const jni_buffer =
|
||||
context->buffer_manager.GetBuffer(buffer_id);
|
||||
jni_buffer->SetFrameData(*decoder_buffer);
|
||||
env->CallVoidMethod(jOutputBuffer, context->init_for_private_frame_method,
|
||||
decoder_buffer->displayed_width[kPlaneY],
|
||||
decoder_buffer->displayed_height[kPlaneY]);
|
||||
if (env->ExceptionCheck()) {
|
||||
// Exception is thrown in Java when returning from the native call.
|
||||
return kStatusError;
|
||||
}
|
||||
env->SetIntField(jOutputBuffer, context->decoder_private_field, buffer_id);
|
||||
}
|
||||
|
||||
return kStatusOk;
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, gav1RenderFrame, jlong jContext, jobject jSurface,
|
||||
jobject jOutputBuffer) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
const int buffer_id =
|
||||
env->GetIntField(jOutputBuffer, context->decoder_private_field);
|
||||
JniFrameBuffer* const jni_buffer =
|
||||
context->buffer_manager.GetBuffer(buffer_id);
|
||||
|
||||
if (!context->MaybeAcquireNativeWindow(env, jSurface)) {
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
if (context->native_window_width != jni_buffer->DisplayedWidth(kPlaneY) ||
|
||||
context->native_window_height != jni_buffer->DisplayedHeight(kPlaneY)) {
|
||||
if (ANativeWindow_setBuffersGeometry(
|
||||
context->native_window, jni_buffer->DisplayedWidth(kPlaneY),
|
||||
jni_buffer->DisplayedHeight(kPlaneY), kImageFormatYV12)) {
|
||||
context->jni_status_code = kJniStatusANativeWindowError;
|
||||
return kStatusError;
|
||||
}
|
||||
context->native_window_width = jni_buffer->DisplayedWidth(kPlaneY);
|
||||
context->native_window_height = jni_buffer->DisplayedHeight(kPlaneY);
|
||||
}
|
||||
|
||||
ANativeWindow_Buffer native_window_buffer;
|
||||
if (ANativeWindow_lock(context->native_window, &native_window_buffer,
|
||||
/*inOutDirtyBounds=*/nullptr) ||
|
||||
native_window_buffer.bits == nullptr) {
|
||||
context->jni_status_code = kJniStatusANativeWindowError;
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
// Y plane
|
||||
CopyPlane(jni_buffer->Plane(kPlaneY), jni_buffer->Stride(kPlaneY),
|
||||
reinterpret_cast<uint8_t*>(native_window_buffer.bits),
|
||||
native_window_buffer.stride, jni_buffer->DisplayedWidth(kPlaneY),
|
||||
jni_buffer->DisplayedHeight(kPlaneY));
|
||||
|
||||
const int y_plane_size =
|
||||
native_window_buffer.stride * native_window_buffer.height;
|
||||
const int32_t native_window_buffer_uv_height =
|
||||
(native_window_buffer.height + 1) / 2;
|
||||
const int native_window_buffer_uv_stride =
|
||||
Align(native_window_buffer.stride / 2, 16);
|
||||
|
||||
// TODO(b/140606738): Handle monochrome videos.
|
||||
|
||||
// V plane
|
||||
// Since the format for ANativeWindow is YV12, V plane is being processed
|
||||
// before U plane.
|
||||
const int v_plane_height = std::min(native_window_buffer_uv_height,
|
||||
jni_buffer->DisplayedHeight(kPlaneV));
|
||||
CopyPlane(
|
||||
jni_buffer->Plane(kPlaneV), jni_buffer->Stride(kPlaneV),
|
||||
reinterpret_cast<uint8_t*>(native_window_buffer.bits) + y_plane_size,
|
||||
native_window_buffer_uv_stride, jni_buffer->DisplayedWidth(kPlaneV),
|
||||
v_plane_height);
|
||||
|
||||
const int v_plane_size = v_plane_height * native_window_buffer_uv_stride;
|
||||
|
||||
// U plane
|
||||
CopyPlane(jni_buffer->Plane(kPlaneU), jni_buffer->Stride(kPlaneU),
|
||||
reinterpret_cast<uint8_t*>(native_window_buffer.bits) +
|
||||
y_plane_size + v_plane_size,
|
||||
native_window_buffer_uv_stride, jni_buffer->DisplayedWidth(kPlaneU),
|
||||
std::min(native_window_buffer_uv_height,
|
||||
jni_buffer->DisplayedHeight(kPlaneU)));
|
||||
|
||||
if (ANativeWindow_unlockAndPost(context->native_window)) {
|
||||
context->jni_status_code = kJniStatusANativeWindowError;
|
||||
return kStatusError;
|
||||
}
|
||||
|
||||
return kStatusOk;
|
||||
}
|
||||
|
||||
DECODER_FUNC(void, gav1ReleaseFrame, jlong jContext, jobject jOutputBuffer) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
const int buffer_id =
|
||||
env->GetIntField(jOutputBuffer, context->decoder_private_field);
|
||||
env->SetIntField(jOutputBuffer, context->decoder_private_field, -1);
|
||||
context->jni_status_code = context->buffer_manager.ReleaseBuffer(buffer_id);
|
||||
if (context->jni_status_code != kJniStatusOk) {
|
||||
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
|
||||
}
|
||||
}
|
||||
|
||||
DECODER_FUNC(jstring, gav1GetErrorMessage, jlong jContext) {
|
||||
if (jContext == 0) {
|
||||
return env->NewStringUTF("Failed to initialize JNI context.");
|
||||
}
|
||||
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
if (context->libgav1_status_code != kLibgav1StatusOk) {
|
||||
return env->NewStringUTF(
|
||||
libgav1::GetErrorString(context->libgav1_status_code));
|
||||
}
|
||||
if (context->jni_status_code != kJniStatusOk) {
|
||||
return env->NewStringUTF(GetJniErrorMessage(context->jni_status_code));
|
||||
}
|
||||
|
||||
return env->NewStringUTF("None.");
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, gav1CheckError, jlong jContext) {
|
||||
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
|
||||
if (context->libgav1_status_code != kLibgav1StatusOk ||
|
||||
context->jni_status_code != kJniStatusOk) {
|
||||
return kStatusError;
|
||||
}
|
||||
return kStatusOk;
|
||||
}
|
||||
|
||||
// TODO(b/139902005): Add functions for getting libgav1 version and build
|
||||
// configuration once libgav1 ABI provides this information.
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
The cast extension is a [Player][] implementation that controls playback on a
|
||||
Cast receiver app.
|
||||
|
||||
[Player]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/Player.html
|
||||
[Player]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/Player.html
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
|
|
|
|||
|
|
@ -16,34 +16,29 @@ apply plugin: 'com.android.library'
|
|||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// These dependencies are necessary to force the supportLibraryVersion of
|
||||
// com.android.support:support-v4, com.android.support:appcompat-v7 and
|
||||
// com.android.support:mediarouter-v7 to be used. Else older versions are
|
||||
// used, for example:
|
||||
// com.google.android.gms:play-services-cast-framework:11.4.2
|
||||
// |-- com.google.android.gms:play-services-basement:11.4.2
|
||||
// |-- com.android.support:support-v4:25.2.0
|
||||
api 'com.android.support:support-v4:' + supportLibraryVersion
|
||||
api 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
||||
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
||||
api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
|
||||
api 'com.google.android.gms:play-services-cast-framework:17.0.0'
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation 'junit:junit:' + junitVersion
|
||||
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.BasePlayer;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
|
|
@ -28,8 +30,8 @@ import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
|
|||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
|
|
@ -43,40 +45,30 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
|||
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
|
||||
import com.google.android.gms.common.api.PendingResult;
|
||||
import com.google.android.gms.common.api.ResultCallback;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* {@link Player} implementation that communicates with a Cast receiver app.
|
||||
*
|
||||
* <p>The behavior of this class depends on the underlying Cast session, which is obtained from the
|
||||
* Cast context passed to {@link #CastPlayer}. To keep track of the session,
|
||||
* {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
|
||||
* implemented and attached to the player.</p>
|
||||
* injected {@link CastContext}. To keep track of the session, {@link #isCastSessionAvailable()} can
|
||||
* be queried and {@link SessionAvailabilityListener} can be implemented and attached to the player.
|
||||
*
|
||||
* <p> If no session is available, the player state will remain unchanged and calls to methods that
|
||||
* <p>If no session is available, the player state will remain unchanged and calls to methods that
|
||||
* alter it will be ignored. Querying the player state is possible even when no session is
|
||||
* available, in which case, the last observed receiver app state is reported.</p>
|
||||
* available, in which case, the last observed receiver app state is reported.
|
||||
*
|
||||
* <p>Methods should be called on the application's main thread.</p>
|
||||
* <p>Methods should be called on the application's main thread.
|
||||
*/
|
||||
public final class CastPlayer implements Player {
|
||||
|
||||
/**
|
||||
* Listener of changes in the cast session availability.
|
||||
*/
|
||||
public interface SessionAvailabilityListener {
|
||||
|
||||
/**
|
||||
* Called when a cast session becomes available to the player.
|
||||
*/
|
||||
void onCastSessionAvailable();
|
||||
|
||||
/**
|
||||
* Called when the cast session becomes unavailable.
|
||||
*/
|
||||
void onCastSessionUnavailable();
|
||||
public final class CastPlayer extends BasePlayer {
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.cast");
|
||||
}
|
||||
|
||||
private static final String TAG = "CastPlayer";
|
||||
|
|
@ -93,32 +85,31 @@ public final class CastPlayer implements Player {
|
|||
private final CastContext castContext;
|
||||
// TODO: Allow custom implementations of CastTimelineTracker.
|
||||
private final CastTimelineTracker timelineTracker;
|
||||
private final Timeline.Window window;
|
||||
private final Timeline.Period period;
|
||||
|
||||
private RemoteMediaClient remoteMediaClient;
|
||||
|
||||
// Result callbacks.
|
||||
private final StatusListener statusListener;
|
||||
private final SeekResultCallback seekResultCallback;
|
||||
|
||||
// Listeners.
|
||||
private final CopyOnWriteArraySet<EventListener> listeners;
|
||||
private SessionAvailabilityListener sessionAvailabilityListener;
|
||||
// Listeners and notification.
|
||||
private final CopyOnWriteArrayList<ListenerHolder> listeners;
|
||||
private final ArrayList<ListenerNotificationTask> notificationsBatch;
|
||||
private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
|
||||
@Nullable private SessionAvailabilityListener sessionAvailabilityListener;
|
||||
|
||||
// Internal state.
|
||||
private final StateHolder<Boolean> playWhenReady;
|
||||
private final StateHolder<Integer> repeatMode;
|
||||
@Nullable private RemoteMediaClient remoteMediaClient;
|
||||
private CastTimeline currentTimeline;
|
||||
private TrackGroupArray currentTrackGroups;
|
||||
private TrackSelectionArray currentTrackSelection;
|
||||
private int playbackState;
|
||||
private int repeatMode;
|
||||
@Player.State private int playbackState;
|
||||
private int currentWindowIndex;
|
||||
private boolean playWhenReady;
|
||||
private long lastReportedPositionMs;
|
||||
private int pendingSeekCount;
|
||||
private int pendingSeekWindowIndex;
|
||||
private long pendingSeekPositionMs;
|
||||
private boolean waitingForInitialTimeline;
|
||||
|
||||
/**
|
||||
* @param castContext The context from which the cast session is obtained.
|
||||
|
|
@ -126,25 +117,27 @@ public final class CastPlayer implements Player {
|
|||
public CastPlayer(CastContext castContext) {
|
||||
this.castContext = castContext;
|
||||
timelineTracker = new CastTimelineTracker();
|
||||
window = new Timeline.Window();
|
||||
period = new Timeline.Period();
|
||||
statusListener = new StatusListener();
|
||||
seekResultCallback = new SeekResultCallback();
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
SessionManager sessionManager = castContext.getSessionManager();
|
||||
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
|
||||
CastSession session = sessionManager.getCurrentCastSession();
|
||||
remoteMediaClient = session != null ? session.getRemoteMediaClient() : null;
|
||||
listeners = new CopyOnWriteArrayList<>();
|
||||
notificationsBatch = new ArrayList<>();
|
||||
ongoingNotificationsTasks = new ArrayDeque<>();
|
||||
|
||||
playWhenReady = new StateHolder<>(false);
|
||||
repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
|
||||
playbackState = STATE_IDLE;
|
||||
repeatMode = REPEAT_MODE_OFF;
|
||||
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
currentTrackGroups = TrackGroupArray.EMPTY;
|
||||
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
|
||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
updateInternalState();
|
||||
|
||||
SessionManager sessionManager = castContext.getSessionManager();
|
||||
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
|
||||
CastSession session = sessionManager.getCurrentCastSession();
|
||||
setRemoteMediaClient(session != null ? session.getRemoteMediaClient() : null);
|
||||
updateInternalStateAndNotifyIfChanged();
|
||||
}
|
||||
|
||||
// Media Queue manipulation methods.
|
||||
|
|
@ -158,6 +151,7 @@ public final class CastPlayer implements Player {
|
|||
* starts at position 0.
|
||||
* @return The Cast {@code PendingResult}, or null if no session is available.
|
||||
*/
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
|
||||
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
|
||||
}
|
||||
|
|
@ -173,11 +167,11 @@ public final class CastPlayer implements Player {
|
|||
* @param repeatMode The repeat mode for the created media queue.
|
||||
* @return The Cast {@code PendingResult}, or null if no session is available.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> loadItems(MediaQueueItem[] items, int startIndex,
|
||||
long positionMs, @RepeatMode int repeatMode) {
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> loadItems(
|
||||
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
|
||||
if (remoteMediaClient != null) {
|
||||
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
|
||||
waitingForInitialTimeline = true;
|
||||
return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),
|
||||
positionMs, null);
|
||||
}
|
||||
|
|
@ -190,6 +184,7 @@ public final class CastPlayer implements Player {
|
|||
* @param items The items to append.
|
||||
* @return The Cast {@code PendingResult}, or null if no media queue exists.
|
||||
*/
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
|
||||
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
|
||||
}
|
||||
|
|
@ -204,6 +199,7 @@ public final class CastPlayer implements Player {
|
|||
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
||||
* periodId} exist.
|
||||
*/
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
|
||||
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|
||||
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
|
||||
|
|
@ -221,6 +217,7 @@ public final class CastPlayer implements Player {
|
|||
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
||||
* periodId} exist.
|
||||
*/
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> removeItem(int periodId) {
|
||||
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||
return remoteMediaClient.queueRemoveItem(periodId, null);
|
||||
|
|
@ -239,6 +236,7 @@ public final class CastPlayer implements Player {
|
|||
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
||||
* periodId} exist.
|
||||
*/
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
|
||||
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
|
||||
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||
|
|
@ -256,6 +254,7 @@ public final class CastPlayer implements Player {
|
|||
* @return The item that corresponds to the period with the given id, or null if no media queue or
|
||||
* period with id {@code periodId} exist.
|
||||
*/
|
||||
@Nullable
|
||||
public MediaQueueItem getItem(int periodId) {
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
|
||||
|
|
@ -274,69 +273,112 @@ public final class CastPlayer implements Player {
|
|||
/**
|
||||
* Sets a listener for updates on the cast session availability.
|
||||
*
|
||||
* @param listener The {@link SessionAvailabilityListener}.
|
||||
* @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.
|
||||
*/
|
||||
public void setSessionAvailabilityListener(SessionAvailabilityListener listener) {
|
||||
public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
|
||||
sessionAvailabilityListener = listener;
|
||||
}
|
||||
|
||||
// Player implementation.
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public AudioComponent getAudioComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public VideoComponent getVideoComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public TextComponent getTextComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MetadataComponent getMetadataComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Looper getApplicationLooper() {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(EventListener listener) {
|
||||
listeners.add(listener);
|
||||
listeners.addIfAbsent(new ListenerHolder(listener));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(EventListener listener) {
|
||||
listeners.remove(listener);
|
||||
for (ListenerHolder listenerHolder : listeners) {
|
||||
if (listenerHolder.listener.equals(listener)) {
|
||||
listenerHolder.release();
|
||||
listeners.remove(listenerHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Player.State
|
||||
public int getPlaybackState() {
|
||||
return playbackState;
|
||||
}
|
||||
|
||||
@Override
|
||||
@PlaybackSuppressionReason
|
||||
public int getPlaybackSuppressionReason() {
|
||||
return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
@Nullable
|
||||
public ExoPlaybackException getPlaybackError() {
|
||||
return getPlayerError();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ExoPlaybackException getPlayerError() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayWhenReady(boolean playWhenReady) {
|
||||
if (remoteMediaClient == null) {
|
||||
return;
|
||||
}
|
||||
if (playWhenReady) {
|
||||
remoteMediaClient.play();
|
||||
} else {
|
||||
remoteMediaClient.pause();
|
||||
}
|
||||
// We update the local state and send the message to the receiver app, which will cause the
|
||||
// operation to be perceived as synchronous by the user. When the operation reports a result,
|
||||
// the local state will be updated to reflect the state reported by the Cast SDK.
|
||||
setPlayerStateAndNotifyIfChanged(
|
||||
playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, playbackState);
|
||||
flushNotifications();
|
||||
PendingResult<MediaChannelResult> pendingResult =
|
||||
playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause();
|
||||
this.playWhenReady.pendingResultCallback =
|
||||
new ResultCallback<MediaChannelResult>() {
|
||||
@Override
|
||||
public void onResult(MediaChannelResult mediaChannelResult) {
|
||||
if (remoteMediaClient != null) {
|
||||
updatePlayerStateAndNotifyIfChanged(this);
|
||||
flushNotifications();
|
||||
}
|
||||
}
|
||||
};
|
||||
pendingResult.setResultCallback(this.playWhenReady.pendingResultCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getPlayWhenReady() {
|
||||
return playWhenReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToDefaultPosition() {
|
||||
seekTo(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToDefaultPosition(int windowIndex) {
|
||||
seekTo(windowIndex, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(long positionMs) {
|
||||
seekTo(getCurrentWindowIndex(), positionMs);
|
||||
return playWhenReady.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -355,14 +397,13 @@ public final class CastPlayer implements Player {
|
|||
pendingSeekCount++;
|
||||
pendingSeekWindowIndex = windowIndex;
|
||||
pendingSeekPositionMs = positionMs;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
|
||||
}
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
|
||||
} else if (pendingSeekCount == 0) {
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onSeekProcessed();
|
||||
}
|
||||
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
|
||||
}
|
||||
flushNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -375,11 +416,6 @@ public final class CastPlayer implements Player {
|
|||
return PlaybackParameters.DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
stop(/* reset= */ false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(boolean reset) {
|
||||
playbackState = STATE_IDLE;
|
||||
|
|
@ -418,14 +454,32 @@ public final class CastPlayer implements Player {
|
|||
|
||||
@Override
|
||||
public void setRepeatMode(@RepeatMode int repeatMode) {
|
||||
if (remoteMediaClient != null) {
|
||||
remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null);
|
||||
if (remoteMediaClient == null) {
|
||||
return;
|
||||
}
|
||||
// We update the local state and send the message to the receiver app, which will cause the
|
||||
// operation to be perceived as synchronous by the user. When the operation reports a result,
|
||||
// the local state will be updated to reflect the state reported by the Cast SDK.
|
||||
setRepeatModeAndNotifyIfChanged(repeatMode);
|
||||
flushNotifications();
|
||||
PendingResult<MediaChannelResult> pendingResult =
|
||||
remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), /* jsonObject= */ null);
|
||||
this.repeatMode.pendingResultCallback =
|
||||
new ResultCallback<MediaChannelResult>() {
|
||||
@Override
|
||||
public void onResult(MediaChannelResult mediaChannelResult) {
|
||||
if (remoteMediaClient != null) {
|
||||
updateRepeatModeAndNotifyIfChanged(this);
|
||||
flushNotifications();
|
||||
}
|
||||
}
|
||||
};
|
||||
pendingResult.setResultCallback(this.repeatMode.pendingResultCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RepeatMode public int getRepeatMode() {
|
||||
return repeatMode;
|
||||
return repeatMode.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -454,11 +508,6 @@ public final class CastPlayer implements Player {
|
|||
return currentTimeline;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable public Object getCurrentManifest() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentPeriodIndex() {
|
||||
return getCurrentWindowIndex();
|
||||
|
|
@ -469,24 +518,11 @@ public final class CastPlayer implements Player {
|
|||
return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextWindowIndex() {
|
||||
return currentTimeline.isEmpty() ? C.INDEX_UNSET
|
||||
: currentTimeline.getNextWindowIndex(getCurrentWindowIndex(), repeatMode, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviousWindowIndex() {
|
||||
return currentTimeline.isEmpty() ? C.INDEX_UNSET
|
||||
: currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false);
|
||||
}
|
||||
|
||||
// TODO: Fill the cast timeline information with ProgressListener's duration updates.
|
||||
// See [Internal: b/65152553].
|
||||
@Override
|
||||
public long getDuration() {
|
||||
return currentTimeline.isEmpty() ? C.TIME_UNSET
|
||||
: currentTimeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
|
||||
return getContentDuration();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -504,24 +540,12 @@ public final class CastPlayer implements Player {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getBufferedPercentage() {
|
||||
long position = getBufferedPosition();
|
||||
long duration = getDuration();
|
||||
return position == C.TIME_UNSET || duration == C.TIME_UNSET
|
||||
public long getTotalBufferedDuration() {
|
||||
long bufferedPosition = getBufferedPosition();
|
||||
long currentPosition = getCurrentPosition();
|
||||
return bufferedPosition == C.TIME_UNSET || currentPosition == C.TIME_UNSET
|
||||
? 0
|
||||
: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCurrentWindowDynamic() {
|
||||
return !currentTimeline.isEmpty()
|
||||
&& currentTimeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCurrentWindowSeekable() {
|
||||
return !currentTimeline.isEmpty()
|
||||
&& currentTimeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
|
||||
: bufferedPosition - currentPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -549,54 +573,93 @@ public final class CastPlayer implements Player {
|
|||
return getCurrentPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentBufferedPosition() {
|
||||
return getBufferedPosition();
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
public void updateInternalState() {
|
||||
private void updateInternalStateAndNotifyIfChanged() {
|
||||
if (remoteMediaClient == null) {
|
||||
// There is no session. We leave the state of the player as it is now.
|
||||
return;
|
||||
}
|
||||
boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
|
||||
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
|
||||
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
|
||||
if (wasPlaying != isPlaying) {
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(listener -> listener.onIsPlayingChanged(isPlaying)));
|
||||
}
|
||||
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
|
||||
updateTimelineAndNotifyIfChanged();
|
||||
|
||||
int playbackState = fetchPlaybackState(remoteMediaClient);
|
||||
boolean playWhenReady = !remoteMediaClient.isPaused();
|
||||
if (this.playbackState != playbackState
|
||||
|| this.playWhenReady != playWhenReady) {
|
||||
this.playbackState = playbackState;
|
||||
this.playWhenReady = playWhenReady;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
|
||||
}
|
||||
int currentWindowIndex = C.INDEX_UNSET;
|
||||
MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
|
||||
if (currentItem != null) {
|
||||
currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId());
|
||||
}
|
||||
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
|
||||
if (this.repeatMode != repeatMode) {
|
||||
this.repeatMode = repeatMode;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onRepeatModeChanged(repeatMode);
|
||||
}
|
||||
if (currentWindowIndex == C.INDEX_UNSET) {
|
||||
// The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.
|
||||
currentWindowIndex = 0;
|
||||
}
|
||||
int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
|
||||
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
|
||||
this.currentWindowIndex = currentWindowIndex;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION);
|
||||
}
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener ->
|
||||
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
|
||||
}
|
||||
if (updateTracksAndSelections()) {
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
|
||||
}
|
||||
if (updateTracksAndSelectionsAndNotifyIfChanged()) {
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
|
||||
}
|
||||
maybeUpdateTimelineAndNotify();
|
||||
flushNotifications();
|
||||
}
|
||||
|
||||
private void maybeUpdateTimelineAndNotify() {
|
||||
/**
|
||||
* Updates {@link #playWhenReady} and {@link #playbackState} to match the Cast {@code
|
||||
* remoteMediaClient} state, and notifies listeners of any state changes.
|
||||
*
|
||||
* <p>This method will only update values whose {@link StateHolder#pendingResultCallback} matches
|
||||
* the given {@code resultCallback}.
|
||||
*/
|
||||
@RequiresNonNull("remoteMediaClient")
|
||||
private void updatePlayerStateAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
|
||||
boolean newPlayWhenReadyValue = playWhenReady.value;
|
||||
if (playWhenReady.acceptsUpdate(resultCallback)) {
|
||||
newPlayWhenReadyValue = !remoteMediaClient.isPaused();
|
||||
playWhenReady.clearPendingResultCallback();
|
||||
}
|
||||
@PlayWhenReadyChangeReason
|
||||
int playWhenReadyChangeReason =
|
||||
newPlayWhenReadyValue != playWhenReady.value
|
||||
? PLAY_WHEN_READY_CHANGE_REASON_REMOTE
|
||||
: PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
|
||||
// We do not mask the playback state, so try setting it regardless of the playWhenReady masking.
|
||||
setPlayerStateAndNotifyIfChanged(
|
||||
newPlayWhenReadyValue, playWhenReadyChangeReason, fetchPlaybackState(remoteMediaClient));
|
||||
}
|
||||
|
||||
@RequiresNonNull("remoteMediaClient")
|
||||
private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
|
||||
if (repeatMode.acceptsUpdate(resultCallback)) {
|
||||
setRepeatModeAndNotifyIfChanged(fetchRepeatMode(remoteMediaClient));
|
||||
repeatMode.clearPendingResultCallback();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTimelineAndNotifyIfChanged() {
|
||||
if (updateTimeline()) {
|
||||
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
|
||||
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
||||
waitingForInitialTimeline = false;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onTimelineChanged(currentTimeline, null, reason);
|
||||
}
|
||||
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
|
||||
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener ->
|
||||
listener.onTimelineChanged(
|
||||
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -607,14 +670,14 @@ public final class CastPlayer implements Player {
|
|||
CastTimeline oldTimeline = currentTimeline;
|
||||
MediaStatus status = getMediaStatus();
|
||||
currentTimeline =
|
||||
status != null ? timelineTracker.getCastTimeline(status) : CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
status != null
|
||||
? timelineTracker.getCastTimeline(remoteMediaClient)
|
||||
: CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
return !oldTimeline.equals(currentTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal tracks and selection and returns whether they have changed.
|
||||
*/
|
||||
private boolean updateTracksAndSelections() {
|
||||
/** Updates the internal tracks and selection and returns whether they have changed. */
|
||||
private boolean updateTracksAndSelectionsAndNotifyIfChanged() {
|
||||
if (remoteMediaClient == null) {
|
||||
// There is no session. We leave the state of the player as it is now.
|
||||
return false;
|
||||
|
|
@ -660,6 +723,38 @@ public final class CastPlayer implements Player {
|
|||
return false;
|
||||
}
|
||||
|
||||
private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
|
||||
if (this.repeatMode.value != repeatMode) {
|
||||
this.repeatMode.value = repeatMode;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(repeatMode)));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void setPlayerStateAndNotifyIfChanged(
|
||||
boolean playWhenReady,
|
||||
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
|
||||
@Player.State int playbackState) {
|
||||
boolean playWhenReadyChanged = this.playWhenReady.value != playWhenReady;
|
||||
boolean playbackStateChanged = this.playbackState != playbackState;
|
||||
if (playWhenReadyChanged || playbackStateChanged) {
|
||||
this.playbackState = playbackState;
|
||||
this.playWhenReady.value = playWhenReady;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> {
|
||||
listener.onPlayerStateChanged(playWhenReady, playbackState);
|
||||
if (playbackStateChanged) {
|
||||
listener.onPlaybackStateChanged(playbackState);
|
||||
}
|
||||
if (playWhenReadyChanged) {
|
||||
listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {
|
||||
if (this.remoteMediaClient == remoteMediaClient) {
|
||||
// Do nothing.
|
||||
|
|
@ -676,7 +771,7 @@ public final class CastPlayer implements Player {
|
|||
}
|
||||
remoteMediaClient.addListener(statusListener);
|
||||
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
|
||||
updateInternalState();
|
||||
updateInternalStateAndNotifyIfChanged();
|
||||
} else {
|
||||
if (sessionAvailabilityListener != null) {
|
||||
sessionAvailabilityListener.onCastSessionUnavailable();
|
||||
|
|
@ -684,7 +779,8 @@ public final class CastPlayer implements Player {
|
|||
}
|
||||
}
|
||||
|
||||
private @Nullable MediaStatus getMediaStatus() {
|
||||
@Nullable
|
||||
private MediaStatus getMediaStatus() {
|
||||
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
|
||||
}
|
||||
|
||||
|
|
@ -732,16 +828,6 @@ public final class CastPlayer implements Player {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
|
||||
* there is no media session, returns 0.
|
||||
*/
|
||||
private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
|
||||
Integer currentItemId = mediaStatus != null
|
||||
? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
|
||||
return currentItemId != null ? currentItemId : 0;
|
||||
}
|
||||
|
||||
private static boolean isTrackActive(long id, long[] activeTrackIds) {
|
||||
for (long activeTrackId : activeTrackIds) {
|
||||
if (activeTrackId == id) {
|
||||
|
|
@ -772,8 +858,26 @@ public final class CastPlayer implements Player {
|
|||
}
|
||||
}
|
||||
|
||||
private final class StatusListener implements RemoteMediaClient.Listener,
|
||||
SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
|
||||
private void flushNotifications() {
|
||||
boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
|
||||
ongoingNotificationsTasks.addAll(notificationsBatch);
|
||||
notificationsBatch.clear();
|
||||
if (recursiveNotification) {
|
||||
// This will be handled once the current notification task is finished.
|
||||
return;
|
||||
}
|
||||
while (!ongoingNotificationsTasks.isEmpty()) {
|
||||
ongoingNotificationsTasks.peekFirst().execute();
|
||||
ongoingNotificationsTasks.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
// Internal classes.
|
||||
|
||||
private final class StatusListener
|
||||
implements RemoteMediaClient.Listener,
|
||||
SessionManagerListener<CastSession>,
|
||||
RemoteMediaClient.ProgressListener {
|
||||
|
||||
// RemoteMediaClient.ProgressListener implementation.
|
||||
|
||||
|
|
@ -786,7 +890,7 @@ public final class CastPlayer implements Player {
|
|||
|
||||
@Override
|
||||
public void onStatusUpdated() {
|
||||
updateInternalState();
|
||||
updateInternalStateAndNotifyIfChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -794,7 +898,7 @@ public final class CastPlayer implements Player {
|
|||
|
||||
@Override
|
||||
public void onQueueStatusUpdated() {
|
||||
maybeUpdateTimelineAndNotify();
|
||||
updateTimelineAndNotifyIfChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -806,7 +910,6 @@ public final class CastPlayer implements Player {
|
|||
@Override
|
||||
public void onAdBreakStatusUpdated() {}
|
||||
|
||||
|
||||
// SessionManagerListener implementation.
|
||||
|
||||
@Override
|
||||
|
|
@ -858,12 +961,10 @@ public final class CastPlayer implements Player {
|
|||
|
||||
}
|
||||
|
||||
// Result callbacks hooks.
|
||||
|
||||
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
|
||||
|
||||
@Override
|
||||
public void onResult(@NonNull MediaChannelResult result) {
|
||||
public void onResult(MediaChannelResult result) {
|
||||
int statusCode = result.getStatus().getStatusCode();
|
||||
if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
|
||||
Log.e(TAG, "Seek failed. Error code " + statusCode + ": "
|
||||
|
|
@ -872,11 +973,62 @@ public final class CastPlayer implements Player {
|
|||
if (--pendingSeekCount == 0) {
|
||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onSeekProcessed();
|
||||
}
|
||||
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
|
||||
flushNotifications();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Holds the value and the masking status of a specific part of the {@link CastPlayer} state. */
|
||||
private static final class StateHolder<T> {
|
||||
|
||||
/** The user-facing value of a specific part of the {@link CastPlayer} state. */
|
||||
public T value;
|
||||
|
||||
/**
|
||||
* If {@link #value} is being masked, holds the result callback for the operation that triggered
|
||||
* the masking. Or null if {@link #value} is not being masked.
|
||||
*/
|
||||
@Nullable public ResultCallback<MediaChannelResult> pendingResultCallback;
|
||||
|
||||
public StateHolder(T initialValue) {
|
||||
value = initialValue;
|
||||
}
|
||||
|
||||
public void clearPendingResultCallback() {
|
||||
pendingResultCallback = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this state holder accepts updates coming from the given result callback.
|
||||
*
|
||||
* <p>A null {@code resultCallback} means that the update is a regular receiver state update, in
|
||||
* which case the update will only be accepted if {@link #value} is not being masked. If {@link
|
||||
* #value} is being masked, the update will only be accepted if {@code resultCallback} is the
|
||||
* same as the {@link #pendingResultCallback}.
|
||||
*
|
||||
* @param resultCallback A result callback. May be null if the update comes from a regular
|
||||
* receiver status update.
|
||||
*/
|
||||
public boolean acceptsUpdate(@Nullable ResultCallback<?> resultCallback) {
|
||||
return pendingResultCallback == resultCallback;
|
||||
}
|
||||
}
|
||||
|
||||
private final class ListenerNotificationTask {
|
||||
|
||||
private final Iterator<ListenerHolder> listenersSnapshot;
|
||||
private final ListenerInvocation listenerInvocation;
|
||||
|
||||
private ListenerNotificationTask(ListenerInvocation listenerInvocation) {
|
||||
this.listenersSnapshot = listeners.iterator();
|
||||
this.listenerInvocation = listenerInvocation;
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
while (listenersSnapshot.hasNext()) {
|
||||
listenersSnapshot.next().invoke(listenerInvocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,53 +15,101 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import android.util.SparseIntArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link Timeline} for Cast media queues.
|
||||
*/
|
||||
/* package */ final class CastTimeline extends Timeline {
|
||||
|
||||
/** Holds {@link Timeline} related data for a Cast media item. */
|
||||
public static final class ItemData {
|
||||
|
||||
/** Holds no media information. */
|
||||
public static final ItemData EMPTY = new ItemData();
|
||||
|
||||
/** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
|
||||
public final long durationUs;
|
||||
/**
|
||||
* The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.
|
||||
*/
|
||||
public final long defaultPositionUs;
|
||||
/** Whether the item is live content, or {@code false} if unknown. */
|
||||
public final boolean isLive;
|
||||
|
||||
private ItemData() {
|
||||
this(
|
||||
/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */
|
||||
C.TIME_UNSET,
|
||||
/* isLive= */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param durationUs See {@link #durationsUs}.
|
||||
* @param defaultPositionUs See {@link #defaultPositionUs}.
|
||||
* @param isLive See {@link #isLive}.
|
||||
*/
|
||||
public ItemData(long durationUs, long defaultPositionUs, boolean isLive) {
|
||||
this.durationUs = durationUs;
|
||||
this.defaultPositionUs = defaultPositionUs;
|
||||
this.isLive = isLive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this instance with the given values.
|
||||
*
|
||||
* @param durationUs The duration in microseconds, or {@link C#TIME_UNSET} if unknown.
|
||||
* @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET}
|
||||
* if unknown.
|
||||
* @param isLive Whether the item is live, or {@code false} if unknown.
|
||||
*/
|
||||
public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boolean isLive) {
|
||||
if (durationUs == this.durationUs
|
||||
&& defaultPositionUs == this.defaultPositionUs
|
||||
&& isLive == this.isLive) {
|
||||
return this;
|
||||
}
|
||||
return new ItemData(durationUs, defaultPositionUs, isLive);
|
||||
}
|
||||
}
|
||||
|
||||
/** {@link Timeline} for a cast queue that has no items. */
|
||||
public static final CastTimeline EMPTY_CAST_TIMELINE =
|
||||
new CastTimeline(
|
||||
Collections.<MediaQueueItem>emptyList(), Collections.<String, Long>emptyMap());
|
||||
new CastTimeline(new int[0], new SparseArray<>());
|
||||
|
||||
private final SparseIntArray idsToIndex;
|
||||
private final int[] ids;
|
||||
private final long[] durationsUs;
|
||||
private final long[] defaultPositionsUs;
|
||||
private final boolean[] isLive;
|
||||
|
||||
/**
|
||||
* @param items A list of cast media queue items to represent.
|
||||
* @param contentIdToDurationUsMap A map of content id to duration in microseconds.
|
||||
* Creates a Cast timeline from the given data.
|
||||
*
|
||||
* @param itemIds The ids of the items in the timeline.
|
||||
* @param itemIdToData Maps item ids to {@link ItemData}.
|
||||
*/
|
||||
public CastTimeline(List<MediaQueueItem> items, Map<String, Long> contentIdToDurationUsMap) {
|
||||
int itemCount = items.size();
|
||||
int index = 0;
|
||||
public CastTimeline(int[] itemIds, SparseArray<ItemData> itemIdToData) {
|
||||
int itemCount = itemIds.length;
|
||||
idsToIndex = new SparseIntArray(itemCount);
|
||||
ids = new int[itemCount];
|
||||
ids = Arrays.copyOf(itemIds, itemCount);
|
||||
durationsUs = new long[itemCount];
|
||||
defaultPositionsUs = new long[itemCount];
|
||||
for (MediaQueueItem item : items) {
|
||||
int itemId = item.getItemId();
|
||||
ids[index] = itemId;
|
||||
idsToIndex.put(itemId, index);
|
||||
MediaInfo mediaInfo = item.getMedia();
|
||||
String contentId = mediaInfo.getContentId();
|
||||
durationsUs[index] =
|
||||
contentIdToDurationUsMap.containsKey(contentId)
|
||||
? contentIdToDurationUsMap.get(contentId)
|
||||
: CastUtils.getStreamDurationUs(mediaInfo);
|
||||
defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
|
||||
index++;
|
||||
isLive = new boolean[itemCount];
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
int id = ids[i];
|
||||
idsToIndex.put(id, i);
|
||||
ItemData data = itemIdToData.get(id, ItemData.EMPTY);
|
||||
durationsUs[i] = data.durationUs;
|
||||
defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs;
|
||||
isLive[i] = data.isLive;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,12 +121,24 @@ import java.util.Map;
|
|||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(int windowIndex, Window window, boolean setIds,
|
||||
long defaultPositionProjectionUs) {
|
||||
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
|
||||
long durationUs = durationsUs[windowIndex];
|
||||
boolean isDynamic = durationUs == C.TIME_UNSET;
|
||||
return window.set(ids[windowIndex], C.TIME_UNSET, C.TIME_UNSET, !isDynamic, isDynamic,
|
||||
defaultPositionsUs[windowIndex], durationUs, windowIndex, windowIndex, 0);
|
||||
return window.set(
|
||||
/* uid= */ ids[windowIndex],
|
||||
/* tag= */ ids[windowIndex],
|
||||
/* manifest= */ null,
|
||||
/* presentationStartTimeMs= */ C.TIME_UNSET,
|
||||
/* windowStartTimeMs= */ C.TIME_UNSET,
|
||||
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
|
||||
/* isSeekable= */ !isDynamic,
|
||||
isDynamic,
|
||||
isLive[windowIndex],
|
||||
defaultPositionsUs[windowIndex],
|
||||
durationUs,
|
||||
/* firstPeriodIndex= */ windowIndex,
|
||||
/* lastPeriodIndex= */ windowIndex,
|
||||
/* positionInFirstPeriodUs= */ 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -97,10 +157,15 @@ import java.util.Map;
|
|||
return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getUidOfPeriod(int periodIndex) {
|
||||
return ids[periodIndex];
|
||||
}
|
||||
|
||||
// equals and hashCode implementations.
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
} else if (!(other instanceof CastTimeline)) {
|
||||
|
|
@ -109,7 +174,8 @@ import java.util.Map;
|
|||
CastTimeline that = (CastTimeline) other;
|
||||
return Arrays.equals(ids, that.ids)
|
||||
&& Arrays.equals(durationsUs, that.durationsUs)
|
||||
&& Arrays.equals(defaultPositionsUs, that.defaultPositionsUs);
|
||||
&& Arrays.equals(defaultPositionsUs, that.defaultPositionsUs)
|
||||
&& Arrays.equals(isLive, that.isLive);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -117,6 +183,7 @@ import java.util.Map;
|
|||
int result = Arrays.hashCode(ids);
|
||||
result = 31 * result + Arrays.hashCode(durationsUs);
|
||||
result = 31 * result + Arrays.hashCode(defaultPositionsUs);
|
||||
result = 31 * result + Arrays.hashCode(isLive);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,53 +15,94 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import java.util.HashMap;
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Creates {@link CastTimeline}s from cast receiver app media status.
|
||||
* Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.
|
||||
*
|
||||
* <p>This class keeps track of the duration reported by the current item to fill any missing
|
||||
* durations in the media queue items [See internal: b/65152553].
|
||||
*/
|
||||
/* package */ final class CastTimelineTracker {
|
||||
|
||||
private final HashMap<String, Long> contentIdToDurationUsMap;
|
||||
private final HashSet<String> scratchContentIdSet;
|
||||
private final SparseArray<CastTimeline.ItemData> itemIdToData;
|
||||
|
||||
public CastTimelineTracker() {
|
||||
contentIdToDurationUsMap = new HashMap<>();
|
||||
scratchContentIdSet = new HashSet<>();
|
||||
itemIdToData = new SparseArray<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link CastTimeline} that represent the given {@code status}.
|
||||
* Returns a {@link CastTimeline} that represents the state of the given {@code
|
||||
* remoteMediaClient}.
|
||||
*
|
||||
* @param status The Cast media status.
|
||||
* @return A {@link CastTimeline} that represent the given {@code status}.
|
||||
* <p>Returned timelines may contain values obtained from {@code remoteMediaClient} in previous
|
||||
* invocations of this method.
|
||||
*
|
||||
* @param remoteMediaClient The Cast media client.
|
||||
* @return A {@link CastTimeline} that represents the given {@code remoteMediaClient} status.
|
||||
*/
|
||||
public CastTimeline getCastTimeline(MediaStatus status) {
|
||||
MediaInfo mediaInfo = status.getMediaInfo();
|
||||
List<MediaQueueItem> items = status.getQueueItems();
|
||||
removeUnusedDurationEntries(items);
|
||||
|
||||
if (mediaInfo != null) {
|
||||
String contentId = mediaInfo.getContentId();
|
||||
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
|
||||
contentIdToDurationUsMap.put(contentId, durationUs);
|
||||
public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) {
|
||||
int[] itemIds = remoteMediaClient.getMediaQueue().getItemIds();
|
||||
if (itemIds.length > 0) {
|
||||
// Only remove unused items when there is something in the queue to avoid removing all entries
|
||||
// if the remote media client clears the queue temporarily. See [Internal ref: b/128825216].
|
||||
removeUnusedItemDataEntries(itemIds);
|
||||
}
|
||||
return new CastTimeline(items, contentIdToDurationUsMap);
|
||||
|
||||
// TODO: Reset state when the app instance changes [Internal ref: b/129672468].
|
||||
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
|
||||
if (mediaStatus == null) {
|
||||
return CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
}
|
||||
|
||||
int currentItemId = mediaStatus.getCurrentItemId();
|
||||
updateItemData(
|
||||
currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET);
|
||||
|
||||
for (MediaQueueItem item : mediaStatus.getQueueItems()) {
|
||||
long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
|
||||
updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs);
|
||||
}
|
||||
|
||||
return new CastTimeline(itemIds, itemIdToData);
|
||||
}
|
||||
|
||||
private void removeUnusedDurationEntries(List<MediaQueueItem> items) {
|
||||
scratchContentIdSet.clear();
|
||||
for (MediaQueueItem item : items) {
|
||||
scratchContentIdSet.add(item.getMedia().getContentId());
|
||||
private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) {
|
||||
CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY);
|
||||
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
|
||||
if (durationUs == C.TIME_UNSET) {
|
||||
durationUs = previousData.durationUs;
|
||||
}
|
||||
boolean isLive =
|
||||
mediaInfo == null
|
||||
? previousData.isLive
|
||||
: mediaInfo.getStreamType() == MediaInfo.STREAM_TYPE_LIVE;
|
||||
if (defaultPositionUs == C.TIME_UNSET) {
|
||||
defaultPositionUs = previousData.defaultPositionUs;
|
||||
}
|
||||
itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive));
|
||||
}
|
||||
|
||||
private void removeUnusedItemDataEntries(int[] itemIds) {
|
||||
HashSet<Integer> scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2);
|
||||
for (int id : itemIds) {
|
||||
scratchItemIds.add(id);
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
while (index < itemIdToData.size()) {
|
||||
if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
|
||||
itemIdToData.removeAt(index);
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
contentIdToDurationUsMap.keySet().retainAll(scratchContentIdSet);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
|
|
@ -31,11 +32,13 @@ import com.google.android.gms.cast.MediaTrack;
|
|||
* unknown or not applicable.
|
||||
*
|
||||
* @param mediaInfo The media info to get the duration from.
|
||||
* @return The duration in microseconds.
|
||||
* @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable.
|
||||
*/
|
||||
public static long getStreamDurationUs(MediaInfo mediaInfo) {
|
||||
long durationMs =
|
||||
mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION;
|
||||
public static long getStreamDurationUs(@Nullable MediaInfo mediaInfo) {
|
||||
if (mediaInfo == null) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
long durationMs = mediaInfo.getStreamDuration();
|
||||
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +92,7 @@ import com.google.android.gms.cast.MediaTrack;
|
|||
case CastStatusCodes.UNKNOWN_ERROR:
|
||||
return "An unknown, unexpected error has occurred.";
|
||||
default:
|
||||
return "Unknown: " + statusCode;
|
||||
return CastStatusCodes.getStatusCodeString(statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,8 +104,16 @@ import com.google.android.gms.cast.MediaTrack;
|
|||
* @return The equivalent {@link Format}.
|
||||
*/
|
||||
public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
|
||||
return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(),
|
||||
null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage());
|
||||
return Format.createContainerFormat(
|
||||
mediaTrack.getContentId(),
|
||||
/* label= */ null,
|
||||
mediaTrack.getContentType(),
|
||||
/* sampleMimeType= */ null,
|
||||
/* codecs= */ null,
|
||||
/* bitrate= */ Format.NO_VALUE,
|
||||
/* selectionFlags= */ 0,
|
||||
/* roleFlags= */ 0,
|
||||
mediaTrack.getLanguage());
|
||||
}
|
||||
|
||||
private CastUtils() {}
|
||||
|
|
|
|||