mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
Update to androidx.media3
PiperOrigin-RevId: 405656499
This commit is contained in:
parent
68729ecd49
commit
933e207b3e
10
.github/ISSUE_TEMPLATE/bug.md
vendored
10
.github/ISSUE_TEMPLATE/bug.md
vendored
@ -13,16 +13,14 @@ Before filing a bug:
|
||||
-------------------------
|
||||
|
||||
- Search existing issues, including issues that are closed:
|
||||
https://github.com/androidx/media/issues?q=is%3Aissue
|
||||
- For ExoPlayer-related bugs, please also check the ExoPlayer tracker:
|
||||
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
|
||||
- Consult our developer website: https://exoplayer.dev/
|
||||
- Check the supported formats: https://exoplayer.dev/supported-formats.html
|
||||
- Try playing problematic media in the demo app:
|
||||
http://exoplayer.dev/demo-application.html
|
||||
|
||||
When reporting a bug:
|
||||
-------------------------
|
||||
|
||||
Describe how the issue can be reproduced, ideally using the ExoPlayer demo app
|
||||
Describe how the issue can be reproduced, ideally using one of the demo apps
|
||||
or a small sample app that you’re able to share as source code on GitHub. To
|
||||
increase the chance of your issue getting attention, please also include:
|
||||
|
||||
@ -34,7 +32,7 @@ increase the chance of your issue getting attention, please also include:
|
||||
- DRM scheme and license server URL
|
||||
- Authentication HTTP headers
|
||||
|
||||
- ExoPlayer version number
|
||||
- AndroidX Media version number
|
||||
- Android version
|
||||
- Android device
|
||||
|
||||
|
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -9,8 +9,12 @@ assignees: ''
|
||||
Before filing a feature request:
|
||||
-----------------------
|
||||
- Search existing open issues, specifically with the label ‘enhancement’:
|
||||
https://github.com/androidx/media/labels/enhancement
|
||||
- For ExoPlayer-related feature requests, please also check the ExoPlayer
|
||||
tracker:
|
||||
https://github.com/google/ExoPlayer/labels/enhancement
|
||||
- Search existing pull requests: https://github.com/google/ExoPlayer/pulls
|
||||
- Search existing pull requests: https://github.com/androidx/media/pulls,
|
||||
https://github.com/google/ExoPlayer/pulls
|
||||
|
||||
When filing a feature request:
|
||||
-----------------------
|
||||
|
6
.github/ISSUE_TEMPLATE/question.md
vendored
6
.github/ISSUE_TEMPLATE/question.md
vendored
@ -14,9 +14,9 @@ Before filing a question:
|
||||
|
||||
- Ask general Android development questions on Stack Overflow
|
||||
- Search existing issues, including issues that are closed
|
||||
https://github.com/androidx/media/issues?q=is%3Aissue
|
||||
- For ExoPlayer-related questions, please also check the ExoPlayer tracker:
|
||||
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
|
||||
- Consult our developer website (https://exoplayer.dev/) and Javadoc
|
||||
(https://exoplayer.dev/doc/reference/)
|
||||
|
||||
When filing a question:
|
||||
-------------------------
|
||||
@ -34,7 +34,7 @@ In case your question is related to a piece of media:
|
||||
- DRM scheme and license server URL
|
||||
- Authentication HTTP headers
|
||||
|
||||
Don't forget to check supported formats and devices
|
||||
Don't forget to check ExoPlayer's supported formats and devices, if applicable
|
||||
(https://exoplayer.dev/supported-formats.html).
|
||||
|
||||
If there's something you don't want to post publicly, please submit the issue,
|
||||
|
@ -1,11 +1,14 @@
|
||||
# How to contribute #
|
||||
# How to contribute
|
||||
|
||||
## Reporting issues ##
|
||||
## Reporting issues
|
||||
|
||||
We use the [GitHub issue tracker](https://github.com/google/ExoPlayer/issues)
|
||||
to track bugs, feature requests and questions.
|
||||
We use the [AndroidX Media issue tracker][] to track bugs, feature requests and
|
||||
questions.
|
||||
|
||||
Before filing a new issue, please search the tracker to check if it's already
|
||||
We are still handling ExoPlayer issues on the [ExoPlayer GitHub issue tracker][]
|
||||
while the ExoPlayer and AndroidX Media projects coexist.
|
||||
|
||||
Before filing a new issue, please search the trackers to check if it's already
|
||||
covered by an existing report. Avoiding duplicates helps us maximize the time we
|
||||
can spend fixing bugs and adding new features.
|
||||
|
||||
@ -13,13 +16,16 @@ When filing an issue, be sure to provide enough information for us to
|
||||
efficiently diagnose and reproduce the problem. In particular, please include
|
||||
all of the information requested in the issue template.
|
||||
|
||||
## Pull requests ##
|
||||
[AndroidX Media issue tracker]: https://github.com/androidx/media/issues
|
||||
[ExoPlayer GitHub issue tracker]: https://github.com/google/ExoPlayer/issues
|
||||
|
||||
## Pull requests
|
||||
|
||||
We will also consider high quality pull requests. These should normally merge
|
||||
into the `dev-v2` branch. Before a pull request can be accepted you must submit
|
||||
into the `main` branch. Before a pull request can be accepted you must submit
|
||||
a Contributor License Agreement, as described below.
|
||||
|
||||
## Contributor license agreement ##
|
||||
## Contributor license agreement
|
||||
|
||||
Contributions to any Google project must be accompanied by a Contributor
|
||||
License Agreement. This is not a copyright **assignment**, it simply gives
|
||||
|
147
README.md
147
README.md
@ -1,86 +1,73 @@
|
||||
# ExoPlayer <img src="https://img.shields.io/github/v/release/google/ExoPlayer.svg?label=latest"/>
|
||||
# AndroidX Media
|
||||
|
||||
ExoPlayer is an application level media player for Android. It provides an
|
||||
alternative to Android’s MediaPlayer API for playing audio and video both
|
||||
locally and over the Internet. ExoPlayer supports features not currently
|
||||
supported by Android’s MediaPlayer API, including DASH and SmoothStreaming
|
||||
adaptive playbacks. Unlike the MediaPlayer API, ExoPlayer is easy to customize
|
||||
and extend, and can be updated through Play Store application updates.
|
||||
AndroidX Media is a collection of libraries for implementing media use cases on
|
||||
Android, including local playback (via ExoPlayer) and media sessions.
|
||||
|
||||
## Documentation
|
||||
## Current status
|
||||
|
||||
* The [developer guide][] provides a wealth of information.
|
||||
* The [class reference][] documents ExoPlayer classes.
|
||||
* The [release notes][] document the major changes in each release.
|
||||
* Follow our [developer blog][] to keep up to date with the latest ExoPlayer
|
||||
developments!
|
||||
AndroidX Media is in currently in alpha and we welcome your feedback via the
|
||||
[issue tracker][]. Please consult the [release notes][] for more details about
|
||||
the alpha release.
|
||||
|
||||
[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
|
||||
ExoPlayer's new home will be in AndroidX Media, but for now we are publishing it
|
||||
both in AndroidX Media and via the existing [ExoPlayer project][]. While
|
||||
AndroidX Media is in alpha we recommend that production apps using ExoPlayer
|
||||
continue to depend on the existing ExoPlayer project. We are still handling
|
||||
ExoPlayer issues on the [ExoPlayer issue tracker][].
|
||||
|
||||
## Using ExoPlayer
|
||||
Updated documentation, including information on migration and a developer guide,
|
||||
is coming soon.
|
||||
|
||||
ExoPlayer modules can be obtained from [the Google Maven repository][]. It's
|
||||
also possible to clone the repository and depend on the modules locally.
|
||||
For a high level overview of the initial version of AndroidX Media please see
|
||||
the Android Dev Summit talk [What's next for AndroidX Media and ExoPlayer][].
|
||||
|
||||
[release notes]: RELEASENOTES.md
|
||||
[issue tracker]: https://github.com/androidx/media/issues/new
|
||||
[ExoPlayer project]: https://github.com/google/ExoPlayer
|
||||
[ExoPlayer issue tracker]: https://github.com/google/ExoPlayer/issues
|
||||
[What's next for AndroidX Media and ExoPlayer]: https://developer.android.com/events/dev-summit/technical-talks
|
||||
|
||||
## Using the libraries
|
||||
|
||||
You can get the libraries from [the Google Maven repository][]. It's
|
||||
also possible to clone this GitHub repository and depend on the modules locally.
|
||||
|
||||
[the Google Maven repository]: https://developer.android.com/studio/build/dependencies#google-maven
|
||||
|
||||
### From the Google Maven repository
|
||||
|
||||
#### 1. Add ExoPlayer module dependencies
|
||||
#### 1. Add module dependencies
|
||||
|
||||
The easiest way to get started using ExoPlayer is to add it as a gradle
|
||||
dependency in the `build.gradle` file of your app module. The following will add
|
||||
a dependency to the full library:
|
||||
The easiest way to get started using AndroidX Media is to add gradle
|
||||
dependencies on the libraries you need in the `build.gradle` file of your app
|
||||
module.
|
||||
|
||||
For example, to depend on ExoPlayer with DASH playback support and UI components
|
||||
you can add dependencies on the modules like this:
|
||||
|
||||
```gradle
|
||||
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
||||
implementation 'androidx.media3:media3-exoplayer:1.X.X'
|
||||
implementation 'androidx.media3:media3-exoplayer-dash:1.X.X'
|
||||
implementation 'androidx.media3:media3-ui:1.X.X'
|
||||
```
|
||||
|
||||
where `2.X.X` is your preferred version.
|
||||
where `1.X.X` is your preferred version. All modules must be the same 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
|
||||
only plays DASH content:
|
||||
Please see the [AndroidX Media3 developer.android.com page][] for more
|
||||
information, including a full list of library modules.
|
||||
|
||||
```gradle
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'
|
||||
```
|
||||
This repository includes some modules that depend on external libraries that
|
||||
need to be built manually, and are not available from the Maven repository.
|
||||
Please see the individual READMEs under the [libraries directory][] for more
|
||||
details.
|
||||
|
||||
When depending on individual modules they must all be the same version.
|
||||
|
||||
The available library modules are listed below. Adding a dependency to the full
|
||||
ExoPlayer library is equivalent to adding dependencies on all of the library
|
||||
modules individually.
|
||||
|
||||
* `exoplayer-core`: Core functionality (required).
|
||||
* `exoplayer-dash`: Support for DASH content.
|
||||
* `exoplayer-hls`: Support for HLS content.
|
||||
* `exoplayer-rtsp`: Support for RTSP content.
|
||||
* `exoplayer-smoothstreaming`: Support for SmoothStreaming content.
|
||||
* `exoplayer-transformer`: Media transformation functionality.
|
||||
* `exoplayer-ui`: UI components and resources for use with ExoPlayer.
|
||||
|
||||
In addition to library modules, ExoPlayer has extension modules that depend on
|
||||
external libraries to provide additional functionality. Some extensions are
|
||||
available from the Maven repository, whereas others must be built manually.
|
||||
Browse the [extensions directory][] and their individual READMEs for details.
|
||||
|
||||
More information on the library and extension modules that are available can be
|
||||
found on the [Google Maven ExoPlayer page][].
|
||||
|
||||
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
|
||||
[Google Maven ExoPlayer page]: https://maven.google.com/web/index.html#com.google.android.exoplayer
|
||||
[AndroidX Media3 developer.android.com page]: https://developer.android.com/jetpack/androidx/releases/media3#declaring_dependencies
|
||||
[libraries directory]: libraries
|
||||
|
||||
#### 2. 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
|
||||
`build.gradle` files depending on AndroidX Media, by adding the following to the
|
||||
`android` section:
|
||||
|
||||
```gradle
|
||||
@ -98,45 +85,45 @@ to prevent build errors.
|
||||
### Locally
|
||||
|
||||
Cloning the repository and depending on the modules locally is required when
|
||||
using some ExoPlayer extension modules. It's also a suitable approach if you
|
||||
want to make local changes to ExoPlayer, or if you want to use a development
|
||||
branch.
|
||||
using some libraries. It's also a suitable approach if you want to make local
|
||||
changes, or if you want to use the main branch.
|
||||
|
||||
First, clone the repository into a local directory and checkout the desired
|
||||
branch:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/google/ExoPlayer.git
|
||||
cd ExoPlayer
|
||||
git checkout release-v2
|
||||
git clone https://github.com/androidx/media.git
|
||||
cd media
|
||||
git checkout main
|
||||
```
|
||||
|
||||
Next, add the following to your project's `settings.gradle` file, replacing
|
||||
`path/to/exoplayer` with the path to your local copy:
|
||||
`path/to/media` with the path to your local copy:
|
||||
|
||||
```gradle
|
||||
gradle.ext.exoplayerModulePrefix = 'exoplayer-'
|
||||
apply from: file("path/to/exoplayer/core_settings.gradle")
|
||||
gradle.ext.androidxMediaModulePrefix = 'media-'
|
||||
apply from: file("path/to/media/core_settings.gradle")
|
||||
```
|
||||
|
||||
You should now see the ExoPlayer modules appear as part of your project. You can
|
||||
depend on them as you would on any other local module, for example:
|
||||
You should now see the AndroidX Media modules appear as part of your project.
|
||||
You can depend on them as you would on any other local module, for example:
|
||||
|
||||
```gradle
|
||||
implementation project(':exoplayer-library-core')
|
||||
implementation project(':exoplayer-library-dash')
|
||||
implementation project(':exoplayer-library-ui')
|
||||
implementation project(':media-lib-exoplayer')
|
||||
implementation project(':media-lib-exoplayer-dash')
|
||||
implementation project(':media-lib-ui')
|
||||
```
|
||||
|
||||
## Developing ExoPlayer
|
||||
## Developing AndroidX Media
|
||||
|
||||
#### Project branches
|
||||
|
||||
* Development work happens on the `dev-v2` branch. Pull requests should
|
||||
normally be made to this branch.
|
||||
* The `release-v2` branch holds the most recent release.
|
||||
Development work happens on the `main` branch. Pull requests should normally be
|
||||
made to this branch.
|
||||
|
||||
We plan to add a release branch soon.
|
||||
|
||||
#### Using Android Studio
|
||||
|
||||
To develop ExoPlayer using Android Studio, simply open the ExoPlayer project in
|
||||
the root directory of the repository.
|
||||
To develop AndroidX Media using Android Studio, simply open the project in the
|
||||
root directory of this repository.
|
||||
|
3968
RELEASENOTES.md
3968
RELEASENOTES.md
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,7 @@ buildscript {
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.0'
|
||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.2'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21'
|
||||
}
|
||||
}
|
||||
allprojects {
|
||||
@ -32,7 +33,7 @@ allprojects {
|
||||
}
|
||||
buildDir = "${externalBuildDir}/${project.name}"
|
||||
}
|
||||
group = 'com.google.android.exoplayer'
|
||||
group = 'androidx.media3'
|
||||
}
|
||||
|
||||
apply from: 'javadoc_combined.gradle'
|
||||
|
@ -11,7 +11,7 @@
|
||||
// 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: "$gradle.ext.exoplayerSettingsDir/constants.gradle"
|
||||
apply from: "$gradle.ext.androidxMediaSettingsDir/constants.gradle"
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
|
@ -12,9 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.15.1'
|
||||
releaseVersionCode = 2015001
|
||||
releaseVersion = '1.0.0-alpha01'
|
||||
releaseVersionCode = 1000000
|
||||
minSdkVersion = 16
|
||||
appTargetSdkVersion = 29
|
||||
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
|
||||
@ -36,6 +35,7 @@ project.ext {
|
||||
jsr305Version = '3.0.2'
|
||||
kotlinAnnotationsVersion = '1.5.20'
|
||||
androidxAnnotationVersion = '1.2.0'
|
||||
androidxAnnotationExperimentalVersion = '1.1.0'
|
||||
androidxAppCompatVersion = '1.3.0'
|
||||
androidxCollectionVersion = '1.1.0'
|
||||
androidxCoreVersion = '1.6.0'
|
||||
@ -54,7 +54,7 @@ project.ext {
|
||||
truthVersion = '1.1.3'
|
||||
okhttpVersion = '4.9.1'
|
||||
modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
if (gradle.ext.has('androidxMediaModulePrefix')) {
|
||||
modulePrefix += gradle.ext.androidxMediaModulePrefix
|
||||
}
|
||||
}
|
||||
|
@ -12,82 +12,78 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
def rootDir = file(".")
|
||||
if (!gradle.ext.has('exoplayerSettingsDir')) {
|
||||
gradle.ext.exoplayerSettingsDir = rootDir.getCanonicalPath()
|
||||
if (!gradle.ext.has('androidxMediaSettingsDir')) {
|
||||
gradle.ext.androidxMediaSettingsDir = rootDir.getCanonicalPath()
|
||||
}
|
||||
|
||||
def modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
if (gradle.ext.has('androidxMediaModulePrefix')) {
|
||||
modulePrefix += gradle.ext.androidxMediaModulePrefix
|
||||
}
|
||||
|
||||
include modulePrefix + 'library-common'
|
||||
project(modulePrefix + 'library-common').projectDir = new File(rootDir, 'library/common')
|
||||
include modulePrefix + 'lib-common'
|
||||
project(modulePrefix + 'lib-common').projectDir = new File(rootDir, 'libraries/common')
|
||||
|
||||
include modulePrefix + 'extension-mediasession'
|
||||
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
|
||||
include modulePrefix + 'extension-media2'
|
||||
project(modulePrefix + 'extension-media2').projectDir = new File(rootDir, 'extensions/media2')
|
||||
include modulePrefix + 'lib-session'
|
||||
project(modulePrefix + 'lib-session').projectDir = new File(rootDir, 'libraries/session')
|
||||
|
||||
include modulePrefix + 'library-core'
|
||||
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
|
||||
include modulePrefix + 'library'
|
||||
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
|
||||
include modulePrefix + 'library-dash'
|
||||
project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash')
|
||||
include modulePrefix + 'library-hls'
|
||||
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
|
||||
include modulePrefix + 'library-rtsp'
|
||||
project(modulePrefix + 'library-rtsp').projectDir = new File(rootDir, 'library/rtsp')
|
||||
include modulePrefix + 'library-smoothstreaming'
|
||||
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
|
||||
include modulePrefix + 'extension-ima'
|
||||
project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
|
||||
include modulePrefix + 'extension-workmanager'
|
||||
project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager')
|
||||
include modulePrefix + 'lib-exoplayer'
|
||||
project(modulePrefix + 'lib-exoplayer').projectDir = new File(rootDir, 'libraries/exoplayer')
|
||||
include modulePrefix + 'lib-exoplayer-dash'
|
||||
project(modulePrefix + 'lib-exoplayer-dash').projectDir = new File(rootDir, 'libraries/exoplayer_dash')
|
||||
include modulePrefix + 'lib-exoplayer-hls'
|
||||
project(modulePrefix + 'lib-exoplayer-hls').projectDir = new File(rootDir, 'libraries/exoplayer_hls')
|
||||
include modulePrefix + 'lib-exoplayer-rtsp'
|
||||
project(modulePrefix + 'lib-exoplayer-rtsp').projectDir = new File(rootDir, 'libraries/exoplayer_rtsp')
|
||||
include modulePrefix + 'lib-exoplayer-smoothstreaming'
|
||||
project(modulePrefix + 'lib-exoplayer-smoothstreaming').projectDir = new File(rootDir, 'libraries/exoplayer_smoothstreaming')
|
||||
include modulePrefix + 'lib-exoplayer-ima'
|
||||
project(modulePrefix + 'lib-exoplayer-ima').projectDir = new File(rootDir, 'libraries/exoplayer_ima')
|
||||
include modulePrefix + 'lib-exoplayer-workmanager'
|
||||
project(modulePrefix + 'lib-exoplayer-workmanager').projectDir = new File(rootDir, 'libraries/exoplayer_workmanager')
|
||||
|
||||
include modulePrefix + 'library-ui'
|
||||
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
|
||||
include modulePrefix + 'extension-leanback'
|
||||
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
|
||||
include modulePrefix + 'lib-ui'
|
||||
project(modulePrefix + 'lib-ui').projectDir = new File(rootDir, 'libraries/ui')
|
||||
include modulePrefix + 'lib-ui-leanback'
|
||||
project(modulePrefix + 'lib-ui-leanback').projectDir = new File(rootDir, 'libraries/ui_leanback')
|
||||
|
||||
include modulePrefix + 'library-database'
|
||||
project(modulePrefix + 'library-database').projectDir = new File(rootDir, 'library/database')
|
||||
include modulePrefix + 'lib-database'
|
||||
project(modulePrefix + 'lib-database').projectDir = new File(rootDir, 'libraries/database')
|
||||
|
||||
include modulePrefix + 'library-datasource'
|
||||
project(modulePrefix + 'library-datasource').projectDir = new File(rootDir, 'library/datasource')
|
||||
include modulePrefix + 'extension-cronet'
|
||||
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
|
||||
include modulePrefix + 'extension-rtmp'
|
||||
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
|
||||
include modulePrefix + 'extension-okhttp'
|
||||
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
|
||||
include modulePrefix + 'lib-datasource'
|
||||
project(modulePrefix + 'lib-datasource').projectDir = new File(rootDir, 'libraries/datasource')
|
||||
include modulePrefix + 'lib-datasource-cronet'
|
||||
project(modulePrefix + 'lib-datasource-cronet').projectDir = new File(rootDir, 'libraries/datasource_cronet')
|
||||
include modulePrefix + 'lib-datasource-rtmp'
|
||||
project(modulePrefix + 'lib-datasource-rtmp').projectDir = new File(rootDir, 'libraries/datasource_rtmp')
|
||||
include modulePrefix + 'lib-datasource-okhttp'
|
||||
project(modulePrefix + 'lib-datasource-okhttp').projectDir = new File(rootDir, 'libraries/datasource_okhttp')
|
||||
|
||||
include modulePrefix + 'library-decoder'
|
||||
project(modulePrefix + 'library-decoder').projectDir = new File(rootDir, 'library/decoder')
|
||||
include modulePrefix + 'extension-av1'
|
||||
project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1')
|
||||
include modulePrefix + 'extension-ffmpeg'
|
||||
project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
|
||||
include modulePrefix + 'extension-flac'
|
||||
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
|
||||
include modulePrefix + 'extension-opus'
|
||||
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
|
||||
include modulePrefix + 'extension-vp9'
|
||||
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
|
||||
include modulePrefix + 'lib-decoder'
|
||||
project(modulePrefix + 'lib-decoder').projectDir = new File(rootDir, 'libraries/decoder')
|
||||
include modulePrefix + 'lib-decoder-av1'
|
||||
project(modulePrefix + 'lib-decoder-av1').projectDir = new File(rootDir, 'libraries/decoder_av1')
|
||||
include modulePrefix + 'lib-decoder-ffmpeg'
|
||||
project(modulePrefix + 'lib-decoder-ffmpeg').projectDir = new File(rootDir, 'libraries/decoder_ffmpeg')
|
||||
include modulePrefix + 'lib-decoder-flac'
|
||||
project(modulePrefix + 'lib-decoder-flac').projectDir = new File(rootDir, 'libraries/decoder_flac')
|
||||
include modulePrefix + 'lib-decoder-opus'
|
||||
project(modulePrefix + 'lib-decoder-opus').projectDir = new File(rootDir, 'libraries/decoder_opus')
|
||||
include modulePrefix + 'lib-decoder-vp9'
|
||||
project(modulePrefix + 'lib-decoder-vp9').projectDir = new File(rootDir, 'libraries/decoder_vp9')
|
||||
|
||||
include modulePrefix + 'library-extractor'
|
||||
project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor')
|
||||
include modulePrefix + 'lib-extractor'
|
||||
project(modulePrefix + 'lib-extractor').projectDir = new File(rootDir, 'libraries/extractor')
|
||||
|
||||
include modulePrefix + 'extension-cast'
|
||||
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
|
||||
include modulePrefix + 'lib-cast'
|
||||
project(modulePrefix + 'lib-cast').projectDir = new File(rootDir, 'libraries/cast')
|
||||
|
||||
include modulePrefix + 'library-transformer'
|
||||
project(modulePrefix + 'library-transformer').projectDir = new File(rootDir, 'library/transformer')
|
||||
include modulePrefix + 'lib-transformer'
|
||||
project(modulePrefix + 'lib-transformer').projectDir = new File(rootDir, 'libraries/transformer')
|
||||
|
||||
include modulePrefix + 'robolectricutils'
|
||||
project(modulePrefix + 'robolectricutils').projectDir = new File(rootDir, 'robolectricutils')
|
||||
include modulePrefix + 'testdata'
|
||||
project(modulePrefix + 'testdata').projectDir = new File(rootDir, 'testdata')
|
||||
include modulePrefix + 'testutils'
|
||||
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
|
||||
include modulePrefix + 'test-utils-robolectric'
|
||||
project(modulePrefix + 'test-utils-robolectric').projectDir = new File(rootDir, 'libraries/test_utils_robolectric')
|
||||
include modulePrefix + 'test-data'
|
||||
project(modulePrefix + 'test-data').projectDir = new File(rootDir, 'libraries/test_data')
|
||||
include modulePrefix + 'test-utils'
|
||||
project(modulePrefix + 'test-utils').projectDir = new File(rootDir, 'libraries/test_utils')
|
||||
|
@ -1,25 +1,23 @@
|
||||
# ExoPlayer demos #
|
||||
# Demos
|
||||
|
||||
This directory contains applications that demonstrate how to use ExoPlayer.
|
||||
Browse the individual demos and their READMEs to learn more.
|
||||
This directory contains apps that demonstrate how to use Android media modules,
|
||||
like ExoPlayer. Browse the individual demos and their READMEs to learn more.
|
||||
|
||||
## Running a demo ##
|
||||
## Running a demo
|
||||
|
||||
### From Android Studio ###
|
||||
### From Android Studio
|
||||
|
||||
* File -> New -> Import Project -> Specify the root ExoPlayer folder.
|
||||
* File -> New -> Import Project -> Specify the root `media` folder.
|
||||
* Choose the demo from the run configuration dropdown list.
|
||||
* Click Run.
|
||||
|
||||
### Using gradle from the command line: ###
|
||||
### Using gradle from the command line:
|
||||
|
||||
* Open a Terminal window at the root ExoPlayer folder.
|
||||
* Open a Terminal window at the root `media` folder.
|
||||
* Run `./gradlew projects` to show all projects. Demo projects start with `demo`.
|
||||
* Run `./gradlew :<demo name>:tasks` to view the list of available tasks for
|
||||
the demo project. Choose an install option from the `Install tasks` section.
|
||||
* Run `./gradlew :<demo name>:<install task>`.
|
||||
|
||||
**Example**:
|
||||
|
||||
`./gradlew :demo:installNoDecoderExtensionsDebug` installs the main ExoPlayer
|
||||
demo app in debug mode with no decoder extensions.
|
||||
For example, `./gradlew :demo:installNoDecoderExtensionsDebug` installs the
|
||||
ExoPlayer demo app in debug mode with no optional modules.
|
||||
|
@ -52,13 +52,13 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-rtsp')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'extension-cast')
|
||||
implementation project(modulePrefix + 'lib-exoplayer')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-dash')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-hls')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-rtsp')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-smoothstreaming')
|
||||
implementation project(modulePrefix + 'lib-ui')
|
||||
implementation project(modulePrefix + 'lib-cast')
|
||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion
|
||||
|
@ -14,7 +14,7 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.castdemo">
|
||||
package="androidx.media3.demo.cast">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
@ -26,7 +26,7 @@
|
||||
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="androidx.media3.cast.DefaultCastOptionsProvider"/>
|
||||
|
||||
<activity android:name=".MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
package androidx.media3.demo.cast;
|
||||
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
|
100
demos/cast/src/main/java/androidx/media3/demo/cast/DemoUtil.java
Normal file
100
demos/cast/src/main/java/androidx/media3/demo/cast/DemoUtil.java
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 androidx.media3.demo.cast;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** Utility methods and constants for the Cast demo application. */
|
||||
/* package */ final class DemoUtil {
|
||||
|
||||
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
|
||||
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
|
||||
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
|
||||
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
|
||||
|
||||
/** The list of samples available in the cast demo app. */
|
||||
public static final List<MediaItem> SAMPLES;
|
||||
|
||||
static {
|
||||
ArrayList<MediaItem> samples = new ArrayList<>();
|
||||
|
||||
// Clear content.
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear DASH: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear HLS: Angel one").build())
|
||||
.setMimeType(MIME_TYPE_HLS)
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://html5demos.com/assets/dizzy.mp4")
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear MP4: Dizzy").build())
|
||||
.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"))
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder().setTitle("Widevine DASH cenc: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
|
||||
.setLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||
.build())
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd")
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder().setTitle("Widevine DASH cbc1: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
|
||||
.setLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||
.build())
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd")
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder().setTitle("Widevine DASH cbcs: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
|
||||
.setLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||
.build())
|
||||
.build());
|
||||
|
||||
SAMPLES = Collections.unmodifiableList(samples);
|
||||
}
|
||||
|
||||
private DemoUtil() {}
|
||||
}
|
@ -0,0 +1,316 @@
|
||||
/*
|
||||
* 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 androidx.media3.demo.cast;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.ui.PlayerControlView;
|
||||
import androidx.media3.ui.PlayerView;
|
||||
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.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 ExoPlayer} and supports casting using ExoPlayer's Cast
|
||||
* extension.
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity
|
||||
implements OnClickListener, PlayerManager.Listener {
|
||||
|
||||
private PlayerView localPlayerView;
|
||||
private PlayerControlView castControlView;
|
||||
private PlayerManager playerManager;
|
||||
private RecyclerView mediaQueueList;
|
||||
private MediaQueueListAdapter mediaQueueListAdapter;
|
||||
private CastContext castContext;
|
||||
|
||||
// Activity lifecycle methods.
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Getting the cast context later than onStart can cause device discovery not to take place.
|
||||
try {
|
||||
castContext = CastContext.getSharedInstance(this);
|
||||
} catch (RuntimeException e) {
|
||||
Throwable cause = e.getCause();
|
||||
while (cause != null) {
|
||||
if (cause instanceof DynamiteModule.LoadingException) {
|
||||
setContentView(R.layout.cast_context_error);
|
||||
return;
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
// Unknown error. We propagate it.
|
||||
throw e;
|
||||
}
|
||||
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
localPlayerView = findViewById(R.id.local_player_view);
|
||||
localPlayerView.requestFocus();
|
||||
|
||||
castControlView = findViewById(R.id.cast_control_view);
|
||||
|
||||
mediaQueueList = findViewById(R.id.sample_list);
|
||||
ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback());
|
||||
helper.attachToRecyclerView(mediaQueueList);
|
||||
mediaQueueList.setLayoutManager(new LinearLayoutManager(this));
|
||||
mediaQueueList.setHasFixedSize(true);
|
||||
mediaQueueListAdapter = new MediaQueueListAdapter();
|
||||
|
||||
findViewById(R.id.add_sample_button).setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.menu, menu);
|
||||
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (castContext == null) {
|
||||
// There is no Cast context to work with. Do nothing.
|
||||
return;
|
||||
}
|
||||
playerManager =
|
||||
new PlayerManager(
|
||||
/* listener= */ this,
|
||||
localPlayerView,
|
||||
castControlView,
|
||||
/* context= */ this,
|
||||
castContext);
|
||||
mediaQueueList.setAdapter(mediaQueueListAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (castContext == null) {
|
||||
// Nothing to release.
|
||||
return;
|
||||
}
|
||||
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
|
||||
mediaQueueList.setAdapter(null);
|
||||
playerManager.release();
|
||||
playerManager = null;
|
||||
}
|
||||
|
||||
// Activity input.
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
// If the event was not handled then see if the player view can handle it.
|
||||
return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.add_samples)
|
||||
.setView(buildSampleListView())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
// PlayerManager.Listener implementation.
|
||||
|
||||
@Override
|
||||
public void onQueuePositionChanged(int previousIndex, int newIndex) {
|
||||
if (previousIndex != C.INDEX_UNSET) {
|
||||
mediaQueueListAdapter.notifyItemChanged(previousIndex);
|
||||
}
|
||||
if (newIndex != C.INDEX_UNSET) {
|
||||
mediaQueueListAdapter.notifyItemChanged(newIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnsupportedTrack(int trackType) {
|
||||
if (trackType == C.TRACK_TYPE_AUDIO) {
|
||||
showToast(R.string.error_unsupported_audio);
|
||||
} else if (trackType == C.TRACK_TYPE_VIDEO) {
|
||||
showToast(R.string.error_unsupported_video);
|
||||
}
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void showToast(int messageId) {
|
||||
Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private View buildSampleListView() {
|
||||
View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null);
|
||||
ListView sampleList = dialogList.findViewById(R.id.sample_list);
|
||||
sampleList.setAdapter(new SampleListAdapter(this));
|
||||
sampleList.setOnItemClickListener(
|
||||
(parent, view, position, id) -> {
|
||||
playerManager.addItem(DemoUtil.SAMPLES.get(position));
|
||||
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
|
||||
});
|
||||
return dialogList;
|
||||
}
|
||||
|
||||
// Internal classes.
|
||||
|
||||
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
TextView v =
|
||||
(TextView)
|
||||
LayoutInflater.from(parent.getContext())
|
||||
.inflate(android.R.layout.simple_list_item_1, parent, false);
|
||||
return new QueueItemViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
|
||||
holder.item = Assertions.checkNotNull(playerManager.getItem(position));
|
||||
|
||||
TextView view = holder.textView;
|
||||
view.setText(holder.item.mediaMetadata.title);
|
||||
// TODO: Solve coloring using the theme's ColorStateList.
|
||||
view.setTextColor(
|
||||
ColorUtils.setAlphaComponent(
|
||||
view.getCurrentTextColor(),
|
||||
position == playerManager.getCurrentItemIndex() ? 255 : 100));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return playerManager.getMediaQueueSize();
|
||||
}
|
||||
}
|
||||
|
||||
private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback {
|
||||
|
||||
private int draggingFromPosition;
|
||||
private int draggingToPosition;
|
||||
|
||||
public RecyclerViewCallback() {
|
||||
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END);
|
||||
draggingFromPosition = C.INDEX_UNSET;
|
||||
draggingToPosition = C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(
|
||||
@NonNull RecyclerView list,
|
||||
RecyclerView.ViewHolder origin,
|
||||
RecyclerView.ViewHolder target) {
|
||||
int fromPosition = origin.getAdapterPosition();
|
||||
int toPosition = target.getAdapterPosition();
|
||||
if (draggingFromPosition == C.INDEX_UNSET) {
|
||||
// A drag has started, but changes to the media queue will be reflected in clearView().
|
||||
draggingFromPosition = fromPosition;
|
||||
}
|
||||
draggingToPosition = toPosition;
|
||||
mediaQueueListAdapter.notifyItemMoved(fromPosition, toPosition);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
||||
if (playerManager.removeItem(queueItemHolder.item)) {
|
||||
mediaQueueListAdapter.notifyItemRemoved(position);
|
||||
// Update whichever item took its place, in case it became the new selected item.
|
||||
mediaQueueListAdapter.notifyItemChanged(position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
if (draggingFromPosition != C.INDEX_UNSET) {
|
||||
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
||||
// A drag has ended. We reflect the media queue change in the player.
|
||||
if (!playerManager.moveItem(queueItemHolder.item, draggingToPosition)) {
|
||||
// The move failed. The entire sequence of onMove calls since the drag started needs to be
|
||||
// invalidated.
|
||||
mediaQueueListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
draggingFromPosition = C.INDEX_UNSET;
|
||||
draggingToPosition = C.INDEX_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
|
||||
|
||||
public final TextView textView;
|
||||
public MediaItem item;
|
||||
|
||||
public QueueItemViewHolder(TextView textView) {
|
||||
super(textView);
|
||||
this.textView = textView;
|
||||
textView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
playerManager.selectQueueItem(getAdapterPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SampleListAdapter extends ArrayAdapter<MediaItem> {
|
||||
|
||||
public SampleListAdapter(Context context) {
|
||||
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = super.getView(position, convertView, parent);
|
||||
((TextView) view).setText(Util.castNonNull(getItem(position)).mediaMetadata.title);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
@ -13,24 +13,24 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
package androidx.media3.demo.cast;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.TracksInfo;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import androidx.media3.cast.CastPlayer;
|
||||
import androidx.media3.cast.SessionAvailabilityListener;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.Player.DiscontinuityReason;
|
||||
import androidx.media3.common.Player.TimelineChangeReason;
|
||||
import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.TracksInfo;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.ui.PlayerControlView;
|
||||
import androidx.media3.ui.PlayerView;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import java.util.ArrayList;
|
||||
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package androidx.media3.demo.cast;
|
||||
|
||||
import androidx.media3.common.util.NonNullApi;
|
@ -1,100 +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.castdemo;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** Utility methods and constants for the Cast demo application. */
|
||||
/* package */ final class DemoUtil {
|
||||
|
||||
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
|
||||
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
|
||||
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
|
||||
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
|
||||
|
||||
/** The list of samples available in the cast demo app. */
|
||||
public static final List<MediaItem> SAMPLES;
|
||||
|
||||
static {
|
||||
ArrayList<MediaItem> samples = new ArrayList<>();
|
||||
|
||||
// Clear content.
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear DASH: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear HLS: Angel one").build())
|
||||
.setMimeType(MIME_TYPE_HLS)
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://html5demos.com/assets/dizzy.mp4")
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear MP4: Dizzy").build())
|
||||
.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"))
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder().setTitle("Widevine DASH cenc: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
|
||||
.setLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||
.build())
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd")
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder().setTitle("Widevine DASH cbc1: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
|
||||
.setLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||
.build())
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd")
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder().setTitle("Widevine DASH cbcs: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
|
||||
.setLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||
.build())
|
||||
.build());
|
||||
|
||||
SAMPLES = Collections.unmodifiableList(samples);
|
||||
}
|
||||
|
||||
private DemoUtil() {}
|
||||
}
|
@ -1,316 +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.castdemo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
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 ExoPlayer} and supports casting using ExoPlayer's Cast
|
||||
* extension.
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity
|
||||
implements OnClickListener, PlayerManager.Listener {
|
||||
|
||||
private PlayerView localPlayerView;
|
||||
private PlayerControlView castControlView;
|
||||
private PlayerManager playerManager;
|
||||
private RecyclerView mediaQueueList;
|
||||
private MediaQueueListAdapter mediaQueueListAdapter;
|
||||
private CastContext castContext;
|
||||
|
||||
// Activity lifecycle methods.
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Getting the cast context later than onStart can cause device discovery not to take place.
|
||||
try {
|
||||
castContext = CastContext.getSharedInstance(this);
|
||||
} catch (RuntimeException e) {
|
||||
Throwable cause = e.getCause();
|
||||
while (cause != null) {
|
||||
if (cause instanceof DynamiteModule.LoadingException) {
|
||||
setContentView(R.layout.cast_context_error);
|
||||
return;
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
// Unknown error. We propagate it.
|
||||
throw e;
|
||||
}
|
||||
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
localPlayerView = findViewById(R.id.local_player_view);
|
||||
localPlayerView.requestFocus();
|
||||
|
||||
castControlView = findViewById(R.id.cast_control_view);
|
||||
|
||||
mediaQueueList = findViewById(R.id.sample_list);
|
||||
ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback());
|
||||
helper.attachToRecyclerView(mediaQueueList);
|
||||
mediaQueueList.setLayoutManager(new LinearLayoutManager(this));
|
||||
mediaQueueList.setHasFixedSize(true);
|
||||
mediaQueueListAdapter = new MediaQueueListAdapter();
|
||||
|
||||
findViewById(R.id.add_sample_button).setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.menu, menu);
|
||||
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (castContext == null) {
|
||||
// There is no Cast context to work with. Do nothing.
|
||||
return;
|
||||
}
|
||||
playerManager =
|
||||
new PlayerManager(
|
||||
/* listener= */ this,
|
||||
localPlayerView,
|
||||
castControlView,
|
||||
/* context= */ this,
|
||||
castContext);
|
||||
mediaQueueList.setAdapter(mediaQueueListAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (castContext == null) {
|
||||
// Nothing to release.
|
||||
return;
|
||||
}
|
||||
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
|
||||
mediaQueueList.setAdapter(null);
|
||||
playerManager.release();
|
||||
playerManager = null;
|
||||
}
|
||||
|
||||
// Activity input.
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
// If the event was not handled then see if the player view can handle it.
|
||||
return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.add_samples)
|
||||
.setView(buildSampleListView())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
// PlayerManager.Listener implementation.
|
||||
|
||||
@Override
|
||||
public void onQueuePositionChanged(int previousIndex, int newIndex) {
|
||||
if (previousIndex != C.INDEX_UNSET) {
|
||||
mediaQueueListAdapter.notifyItemChanged(previousIndex);
|
||||
}
|
||||
if (newIndex != C.INDEX_UNSET) {
|
||||
mediaQueueListAdapter.notifyItemChanged(newIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnsupportedTrack(int trackType) {
|
||||
if (trackType == C.TRACK_TYPE_AUDIO) {
|
||||
showToast(R.string.error_unsupported_audio);
|
||||
} else if (trackType == C.TRACK_TYPE_VIDEO) {
|
||||
showToast(R.string.error_unsupported_video);
|
||||
}
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void showToast(int messageId) {
|
||||
Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private View buildSampleListView() {
|
||||
View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null);
|
||||
ListView sampleList = dialogList.findViewById(R.id.sample_list);
|
||||
sampleList.setAdapter(new SampleListAdapter(this));
|
||||
sampleList.setOnItemClickListener(
|
||||
(parent, view, position, id) -> {
|
||||
playerManager.addItem(DemoUtil.SAMPLES.get(position));
|
||||
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
|
||||
});
|
||||
return dialogList;
|
||||
}
|
||||
|
||||
// Internal classes.
|
||||
|
||||
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
TextView v =
|
||||
(TextView)
|
||||
LayoutInflater.from(parent.getContext())
|
||||
.inflate(android.R.layout.simple_list_item_1, parent, false);
|
||||
return new QueueItemViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
|
||||
holder.item = Assertions.checkNotNull(playerManager.getItem(position));
|
||||
|
||||
TextView view = holder.textView;
|
||||
view.setText(holder.item.mediaMetadata.title);
|
||||
// TODO: Solve coloring using the theme's ColorStateList.
|
||||
view.setTextColor(
|
||||
ColorUtils.setAlphaComponent(
|
||||
view.getCurrentTextColor(),
|
||||
position == playerManager.getCurrentItemIndex() ? 255 : 100));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return playerManager.getMediaQueueSize();
|
||||
}
|
||||
}
|
||||
|
||||
private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback {
|
||||
|
||||
private int draggingFromPosition;
|
||||
private int draggingToPosition;
|
||||
|
||||
public RecyclerViewCallback() {
|
||||
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END);
|
||||
draggingFromPosition = C.INDEX_UNSET;
|
||||
draggingToPosition = C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(
|
||||
@NonNull RecyclerView list,
|
||||
RecyclerView.ViewHolder origin,
|
||||
RecyclerView.ViewHolder target) {
|
||||
int fromPosition = origin.getAdapterPosition();
|
||||
int toPosition = target.getAdapterPosition();
|
||||
if (draggingFromPosition == C.INDEX_UNSET) {
|
||||
// A drag has started, but changes to the media queue will be reflected in clearView().
|
||||
draggingFromPosition = fromPosition;
|
||||
}
|
||||
draggingToPosition = toPosition;
|
||||
mediaQueueListAdapter.notifyItemMoved(fromPosition, toPosition);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
||||
if (playerManager.removeItem(queueItemHolder.item)) {
|
||||
mediaQueueListAdapter.notifyItemRemoved(position);
|
||||
// Update whichever item took its place, in case it became the new selected item.
|
||||
mediaQueueListAdapter.notifyItemChanged(position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
if (draggingFromPosition != C.INDEX_UNSET) {
|
||||
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
||||
// A drag has ended. We reflect the media queue change in the player.
|
||||
if (!playerManager.moveItem(queueItemHolder.item, draggingToPosition)) {
|
||||
// The move failed. The entire sequence of onMove calls since the drag started needs to be
|
||||
// invalidated.
|
||||
mediaQueueListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
draggingFromPosition = C.INDEX_UNSET;
|
||||
draggingToPosition = C.INDEX_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
|
||||
|
||||
public final TextView textView;
|
||||
public MediaItem item;
|
||||
|
||||
public QueueItemViewHolder(TextView textView) {
|
||||
super(textView);
|
||||
this.textView = textView;
|
||||
textView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
playerManager.selectQueueItem(getAdapterPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SampleListAdapter extends ArrayAdapter<MediaItem> {
|
||||
|
||||
public SampleListAdapter(Context context) {
|
||||
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = super.getView(position, convertView, parent);
|
||||
((TextView) view).setText(Util.castNonNull(getItem(position)).mediaMetadata.title);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -20,7 +20,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view"
|
||||
<androidx.media3.ui.PlayerView android:id="@+id/local_player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
@ -50,7 +50,7 @@
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view"
|
||||
<androidx.media3.ui.PlayerControlView android:id="@+id/cast_control_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
|
@ -45,12 +45,12 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-rtsp')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'lib-exoplayer')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-dash')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-hls')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-rtsp')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-smoothstreaming')
|
||||
implementation project(modulePrefix + 'lib-ui')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||
|
@ -14,7 +14,7 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.gldemo">
|
||||
package="androidx.media3.demo.gl">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
@ -35,7 +35,7 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.gldemo.action.VIEW"/>
|
||||
<action android:name="androidx.media3.demo.gl.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.gldemo;
|
||||
package androidx.media3.demo.gl;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
@ -25,9 +25,9 @@ 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 androidx.media3.common.C;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
197
demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java
Normal file
197
demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java
Normal file
@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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 androidx.media3.demo.gl;
|
||||
|
||||
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 androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.DefaultDataSource;
|
||||
import androidx.media3.datasource.DefaultHttpDataSource;
|
||||
import androidx.media3.datasource.HttpDataSource;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.dash.DashMediaSource;
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager;
|
||||
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
||||
import androidx.media3.exoplayer.drm.FrameworkMediaDrm;
|
||||
import androidx.media3.exoplayer.drm.HttpMediaDrmCallback;
|
||||
import androidx.media3.exoplayer.source.MediaSource;
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
|
||||
import androidx.media3.exoplayer.util.EventLogger;
|
||||
import androidx.media3.ui.PlayerView;
|
||||
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 = "androidx.media3.demo.gl.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 ExoPlayer 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);
|
||||
DrmSessionManager 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 DefaultHttpDataSource.Factory();
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
|
||||
drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.build(drmCallback);
|
||||
} else {
|
||||
drmSessionManager = DrmSessionManager.DRM_UNSUPPORTED;
|
||||
}
|
||||
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this);
|
||||
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(MediaItem.fromUri(uri));
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
ExoPlayer player = new ExoPlayer.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;
|
||||
}
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.gldemo;
|
||||
package androidx.media3.demo.gl;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.SurfaceTexture;
|
||||
@ -25,13 +25,13 @@ import android.os.Handler;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
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 androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import androidx.media3.common.util.TimedValueQueue;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package androidx.media3.demo.gl;
|
||||
|
||||
import androidx.media3.common.util.NonNullApi;
|
@ -1,197 +0,0 @@
|
||||
/*
|
||||
* 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.ExoPlayer;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
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.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||
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 ExoPlayer 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);
|
||||
DrmSessionManager 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 DefaultHttpDataSource.Factory();
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
|
||||
drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.build(drmCallback);
|
||||
} else {
|
||||
drmSessionManager = DrmSessionManager.DRM_UNSUPPORTED;
|
||||
}
|
||||
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this);
|
||||
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(MediaItem.fromUri(uri));
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
ExoPlayer player = new ExoPlayer.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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.gldemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -20,7 +20,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerView
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -71,20 +71,20 @@ dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
implementation 'com.google.android.material:material:' + androidxMaterialVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-rtsp')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'extension-cronet')
|
||||
implementation project(modulePrefix + 'extension-ima')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-av1')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-ffmpeg')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-flac')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-opus')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-vp9')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-rtmp')
|
||||
implementation project(modulePrefix + 'lib-exoplayer')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-dash')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-hls')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-rtsp')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-smoothstreaming')
|
||||
implementation project(modulePrefix + 'lib-ui')
|
||||
implementation project(modulePrefix + 'lib-datasource-cronet')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-ima')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-av1')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-ffmpeg')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-flac')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-opus')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-vp9')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'lib-datasource-rtmp')
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.demo">
|
||||
package="androidx.media3.demo.main">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
@ -69,7 +69,7 @@
|
||||
android:theme="@style/PlayerTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.demo.action.VIEW"/>
|
||||
<action android:name="androidx.media3.demo.main.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
@ -78,7 +78,7 @@
|
||||
<data android:scheme="file"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.demo.action.VIEW_LIST"/>
|
||||
<action android:name="androidx.media3.demo.main.action.VIEW_LIST"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
@ -86,12 +86,12 @@
|
||||
<service android:name=".DemoDownloadService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART"/>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name="com.google.android.exoplayer2.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
<service android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:exported="true"/>
|
||||
|
||||
|
@ -13,23 +13,23 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
package androidx.media3.demo.main;
|
||||
|
||||
import static com.google.android.exoplayer2.demo.DemoUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID;
|
||||
import static androidx.media3.demo.main.DemoUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
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.scheduler.Requirements;
|
||||
import com.google.android.exoplayer2.scheduler.Scheduler;
|
||||
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
|
||||
import com.google.android.exoplayer2.util.NotificationUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import androidx.media3.common.util.NotificationUtil;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.exoplayer.offline.Download;
|
||||
import androidx.media3.exoplayer.offline.DownloadManager;
|
||||
import androidx.media3.exoplayer.offline.DownloadNotificationHelper;
|
||||
import androidx.media3.exoplayer.offline.DownloadService;
|
||||
import androidx.media3.exoplayer.scheduler.PlatformScheduler;
|
||||
import androidx.media3.exoplayer.scheduler.Requirements;
|
||||
import androidx.media3.exoplayer.scheduler.Scheduler;
|
||||
import java.util.List;
|
||||
|
||||
/** A service for downloading media. */
|
222
demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java
Normal file
222
demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java
Normal file
@ -0,0 +1,222 @@
|
||||
/*
|
||||
* 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 androidx.media3.demo.main;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.database.DatabaseProvider;
|
||||
import androidx.media3.database.StandaloneDatabaseProvider;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.DefaultDataSource;
|
||||
import androidx.media3.datasource.DefaultHttpDataSource;
|
||||
import androidx.media3.datasource.HttpDataSource;
|
||||
import androidx.media3.datasource.cache.Cache;
|
||||
import androidx.media3.datasource.cache.CacheDataSource;
|
||||
import androidx.media3.datasource.cache.NoOpCacheEvictor;
|
||||
import androidx.media3.datasource.cache.SimpleCache;
|
||||
import androidx.media3.datasource.cronet.CronetDataSource;
|
||||
import androidx.media3.datasource.cronet.CronetUtil;
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory;
|
||||
import androidx.media3.exoplayer.RenderersFactory;
|
||||
import androidx.media3.exoplayer.offline.ActionFileUpgradeUtil;
|
||||
import androidx.media3.exoplayer.offline.DefaultDownloadIndex;
|
||||
import androidx.media3.exoplayer.offline.DownloadManager;
|
||||
import androidx.media3.exoplayer.offline.DownloadNotificationHelper;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.chromium.net.CronetEngine;
|
||||
|
||||
/** Utility methods for the demo app. */
|
||||
public final class DemoUtil {
|
||||
|
||||
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
|
||||
|
||||
/**
|
||||
* Whether the demo application uses Cronet for networking. Note that Cronet does not provide
|
||||
* automatic support for cookies (https://github.com/google/ExoPlayer/issues/5975).
|
||||
*
|
||||
* <p>If set to false, the platform's default network stack is used with a {@link CookieManager}
|
||||
* configured in {@link #getHttpDataSourceFactory}.
|
||||
*/
|
||||
private static final boolean USE_CRONET_FOR_NETWORKING = true;
|
||||
|
||||
private static final String TAG = "DemoUtil";
|
||||
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";
|
||||
|
||||
private static DataSource.@MonotonicNonNull Factory dataSourceFactory;
|
||||
private static HttpDataSource.@MonotonicNonNull Factory httpDataSourceFactory;
|
||||
private static @MonotonicNonNull DatabaseProvider databaseProvider;
|
||||
private static @MonotonicNonNull File downloadDirectory;
|
||||
private static @MonotonicNonNull Cache downloadCache;
|
||||
private static @MonotonicNonNull DownloadManager downloadManager;
|
||||
private static @MonotonicNonNull DownloadTracker downloadTracker;
|
||||
private static @MonotonicNonNull DownloadNotificationHelper downloadNotificationHelper;
|
||||
|
||||
/** Returns whether extension renderers should be used. */
|
||||
public static boolean useExtensionRenderers() {
|
||||
return BuildConfig.USE_DECODER_EXTENSIONS;
|
||||
}
|
||||
|
||||
public static RenderersFactory buildRenderersFactory(
|
||||
Context context, 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.getApplicationContext())
|
||||
.setExtensionRendererMode(extensionRendererMode);
|
||||
}
|
||||
|
||||
public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) {
|
||||
if (httpDataSourceFactory == null) {
|
||||
if (USE_CRONET_FOR_NETWORKING) {
|
||||
context = context.getApplicationContext();
|
||||
@Nullable CronetEngine cronetEngine = CronetUtil.buildCronetEngine(context);
|
||||
if (cronetEngine != null) {
|
||||
httpDataSourceFactory =
|
||||
new CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor());
|
||||
}
|
||||
}
|
||||
if (httpDataSourceFactory == null) {
|
||||
// We don't want to use Cronet, or we failed to instantiate a CronetEngine.
|
||||
CookieManager cookieManager = new CookieManager();
|
||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
CookieHandler.setDefault(cookieManager);
|
||||
httpDataSourceFactory = new DefaultHttpDataSource.Factory();
|
||||
}
|
||||
}
|
||||
return httpDataSourceFactory;
|
||||
}
|
||||
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
public static synchronized DataSource.Factory getDataSourceFactory(Context context) {
|
||||
if (dataSourceFactory == null) {
|
||||
context = context.getApplicationContext();
|
||||
DefaultDataSource.Factory upstreamFactory =
|
||||
new DefaultDataSource.Factory(context, getHttpDataSourceFactory(context));
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
}
|
||||
return dataSourceFactory;
|
||||
}
|
||||
|
||||
public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(
|
||||
Context context) {
|
||||
if (downloadNotificationHelper == null) {
|
||||
downloadNotificationHelper =
|
||||
new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID);
|
||||
}
|
||||
return downloadNotificationHelper;
|
||||
}
|
||||
|
||||
public static synchronized DownloadManager getDownloadManager(Context context) {
|
||||
ensureDownloadManagerInitialized(context);
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
public static synchronized DownloadTracker getDownloadTracker(Context context) {
|
||||
ensureDownloadManagerInitialized(context);
|
||||
return downloadTracker;
|
||||
}
|
||||
|
||||
private static synchronized Cache getDownloadCache(Context context) {
|
||||
if (downloadCache == null) {
|
||||
File downloadContentDirectory =
|
||||
new File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY);
|
||||
downloadCache =
|
||||
new SimpleCache(
|
||||
downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider(context));
|
||||
}
|
||||
return downloadCache;
|
||||
}
|
||||
|
||||
private static synchronized void ensureDownloadManagerInitialized(Context context) {
|
||||
if (downloadManager == null) {
|
||||
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider(context));
|
||||
upgradeActionFile(
|
||||
context, DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
|
||||
upgradeActionFile(
|
||||
context,
|
||||
DOWNLOAD_TRACKER_ACTION_FILE,
|
||||
downloadIndex,
|
||||
/* addNewDownloadsAsCompleted= */ true);
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
context,
|
||||
getDatabaseProvider(context),
|
||||
getDownloadCache(context),
|
||||
getHttpDataSourceFactory(context),
|
||||
Executors.newFixedThreadPool(/* nThreads= */ 6));
|
||||
downloadTracker =
|
||||
new DownloadTracker(context, getHttpDataSourceFactory(context), downloadManager);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized void upgradeActionFile(
|
||||
Context context,
|
||||
String fileName,
|
||||
DefaultDownloadIndex downloadIndex,
|
||||
boolean addNewDownloadsAsCompleted) {
|
||||
try {
|
||||
ActionFileUpgradeUtil.upgradeAndDelete(
|
||||
new File(getDownloadDirectory(context), fileName),
|
||||
/* downloadIdProvider= */ null,
|
||||
downloadIndex,
|
||||
/* deleteOnFailure= */ true,
|
||||
addNewDownloadsAsCompleted);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized DatabaseProvider getDatabaseProvider(Context context) {
|
||||
if (databaseProvider == null) {
|
||||
databaseProvider = new StandaloneDatabaseProvider(context);
|
||||
}
|
||||
return databaseProvider;
|
||||
}
|
||||
|
||||
private static synchronized File getDownloadDirectory(Context context) {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getExternalFilesDir(/* type= */ null);
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getFilesDir();
|
||||
}
|
||||
}
|
||||
return downloadDirectory;
|
||||
}
|
||||
|
||||
private static CacheDataSource.Factory buildReadOnlyCacheDataSource(
|
||||
DataSource.Factory upstreamFactory, Cache cache) {
|
||||
return new CacheDataSource.Factory()
|
||||
.setCache(cache)
|
||||
.setUpstreamDataSourceFactory(upstreamFactory)
|
||||
.setCacheWriteDataSinkFactory(null)
|
||||
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
|
||||
}
|
||||
|
||||
private DemoUtil() {}
|
||||
}
|
@ -13,10 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
package androidx.media3.demo.main;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
@ -27,28 +27,28 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.drm.OfflineLicenseHelper;
|
||||
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.DownloadHelper.LiveContentUnsupportedException;
|
||||
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.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import androidx.media3.common.DrmInitData;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.TrackGroup;
|
||||
import androidx.media3.common.TrackGroupArray;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.HttpDataSource;
|
||||
import androidx.media3.exoplayer.RenderersFactory;
|
||||
import androidx.media3.exoplayer.drm.DrmSession;
|
||||
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
|
||||
import androidx.media3.exoplayer.drm.OfflineLicenseHelper;
|
||||
import androidx.media3.exoplayer.offline.Download;
|
||||
import androidx.media3.exoplayer.offline.DownloadCursor;
|
||||
import androidx.media3.exoplayer.offline.DownloadHelper;
|
||||
import androidx.media3.exoplayer.offline.DownloadHelper.LiveContentUnsupportedException;
|
||||
import androidx.media3.exoplayer.offline.DownloadIndex;
|
||||
import androidx.media3.exoplayer.offline.DownloadManager;
|
||||
import androidx.media3.exoplayer.offline.DownloadRequest;
|
||||
import androidx.media3.exoplayer.offline.DownloadService;
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
|
||||
import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
@ -13,19 +13,19 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
package androidx.media3.demo.main;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Util;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@ -38,9 +38,8 @@ public class IntentUtil {
|
||||
|
||||
// Actions.
|
||||
|
||||
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
|
||||
public static final String ACTION_VIEW_LIST =
|
||||
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
||||
public static final String ACTION_VIEW = "androidx.media3.demo.main.action.VIEW";
|
||||
public static final String ACTION_VIEW_LIST = "androidx.media3.demo.main.action.VIEW_LIST";
|
||||
|
||||
// Activity extras.
|
||||
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
@ -13,9 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
package androidx.media3.demo.main;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
@ -31,30 +31,30 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.TracksInfo;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.util.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||
import com.google.android.exoplayer2.util.EventLogger;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import androidx.media3.common.AudioAttributes;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.ErrorMessageProvider;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.TracksInfo;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.RenderersFactory;
|
||||
import androidx.media3.exoplayer.drm.FrameworkMediaDrm;
|
||||
import androidx.media3.exoplayer.ima.ImaAdsLoader;
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||
import androidx.media3.exoplayer.offline.DownloadRequest;
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
|
||||
import androidx.media3.exoplayer.source.MediaSourceFactory;
|
||||
import androidx.media3.exoplayer.source.ads.AdsLoader;
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
|
||||
import androidx.media3.exoplayer.util.DebugTextViewHelper;
|
||||
import androidx.media3.exoplayer.util.EventLogger;
|
||||
import androidx.media3.ui.StyledPlayerControlView;
|
||||
import androidx.media3.ui.StyledPlayerView;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
@ -13,11 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
package androidx.media3.demo.main;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@ -43,17 +43,17 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
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.DataSourceUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.ParserException;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.DataSourceInputStream;
|
||||
import androidx.media3.datasource.DataSourceUtil;
|
||||
import androidx.media3.datasource.DataSpec;
|
||||
import androidx.media3.exoplayer.RenderersFactory;
|
||||
import androidx.media3.exoplayer.offline.DownloadService;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
package androidx.media3.demo.main;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
@ -31,14 +31,14 @@ import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.TrackGroupArray;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride;
|
||||
import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import androidx.media3.ui.TrackSelectionView;
|
||||
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;
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package androidx.media3.demo.main;
|
||||
|
||||
import androidx.media3.common.util.NonNullApi;
|
@ -1,222 +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.content.Context;
|
||||
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.StandaloneDatabaseProvider;
|
||||
import com.google.android.exoplayer2.ext.cronet.CronetDataSource;
|
||||
import com.google.android.exoplayer2.ext.cronet.CronetUtil;
|
||||
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
|
||||
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.chromium.net.CronetEngine;
|
||||
|
||||
/** Utility methods for the demo app. */
|
||||
public final class DemoUtil {
|
||||
|
||||
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
|
||||
|
||||
/**
|
||||
* Whether the demo application uses Cronet for networking. Note that Cronet does not provide
|
||||
* automatic support for cookies (https://github.com/google/ExoPlayer/issues/5975).
|
||||
*
|
||||
* <p>If set to false, the platform's default network stack is used with a {@link CookieManager}
|
||||
* configured in {@link #getHttpDataSourceFactory}.
|
||||
*/
|
||||
private static final boolean USE_CRONET_FOR_NETWORKING = true;
|
||||
|
||||
private static final String TAG = "DemoUtil";
|
||||
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";
|
||||
|
||||
private static DataSource.@MonotonicNonNull Factory dataSourceFactory;
|
||||
private static HttpDataSource.@MonotonicNonNull Factory httpDataSourceFactory;
|
||||
private static @MonotonicNonNull DatabaseProvider databaseProvider;
|
||||
private static @MonotonicNonNull File downloadDirectory;
|
||||
private static @MonotonicNonNull Cache downloadCache;
|
||||
private static @MonotonicNonNull DownloadManager downloadManager;
|
||||
private static @MonotonicNonNull DownloadTracker downloadTracker;
|
||||
private static @MonotonicNonNull DownloadNotificationHelper downloadNotificationHelper;
|
||||
|
||||
/** Returns whether extension renderers should be used. */
|
||||
public static boolean useExtensionRenderers() {
|
||||
return BuildConfig.USE_DECODER_EXTENSIONS;
|
||||
}
|
||||
|
||||
public static RenderersFactory buildRenderersFactory(
|
||||
Context context, 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.getApplicationContext())
|
||||
.setExtensionRendererMode(extensionRendererMode);
|
||||
}
|
||||
|
||||
public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) {
|
||||
if (httpDataSourceFactory == null) {
|
||||
if (USE_CRONET_FOR_NETWORKING) {
|
||||
context = context.getApplicationContext();
|
||||
@Nullable CronetEngine cronetEngine = CronetUtil.buildCronetEngine(context);
|
||||
if (cronetEngine != null) {
|
||||
httpDataSourceFactory =
|
||||
new CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor());
|
||||
}
|
||||
}
|
||||
if (httpDataSourceFactory == null) {
|
||||
// We don't want to use Cronet, or we failed to instantiate a CronetEngine.
|
||||
CookieManager cookieManager = new CookieManager();
|
||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
CookieHandler.setDefault(cookieManager);
|
||||
httpDataSourceFactory = new DefaultHttpDataSource.Factory();
|
||||
}
|
||||
}
|
||||
return httpDataSourceFactory;
|
||||
}
|
||||
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
public static synchronized DataSource.Factory getDataSourceFactory(Context context) {
|
||||
if (dataSourceFactory == null) {
|
||||
context = context.getApplicationContext();
|
||||
DefaultDataSource.Factory upstreamFactory =
|
||||
new DefaultDataSource.Factory(context, getHttpDataSourceFactory(context));
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
}
|
||||
return dataSourceFactory;
|
||||
}
|
||||
|
||||
public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(
|
||||
Context context) {
|
||||
if (downloadNotificationHelper == null) {
|
||||
downloadNotificationHelper =
|
||||
new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID);
|
||||
}
|
||||
return downloadNotificationHelper;
|
||||
}
|
||||
|
||||
public static synchronized DownloadManager getDownloadManager(Context context) {
|
||||
ensureDownloadManagerInitialized(context);
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
public static synchronized DownloadTracker getDownloadTracker(Context context) {
|
||||
ensureDownloadManagerInitialized(context);
|
||||
return downloadTracker;
|
||||
}
|
||||
|
||||
private static synchronized Cache getDownloadCache(Context context) {
|
||||
if (downloadCache == null) {
|
||||
File downloadContentDirectory =
|
||||
new File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY);
|
||||
downloadCache =
|
||||
new SimpleCache(
|
||||
downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider(context));
|
||||
}
|
||||
return downloadCache;
|
||||
}
|
||||
|
||||
private static synchronized void ensureDownloadManagerInitialized(Context context) {
|
||||
if (downloadManager == null) {
|
||||
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider(context));
|
||||
upgradeActionFile(
|
||||
context, DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
|
||||
upgradeActionFile(
|
||||
context,
|
||||
DOWNLOAD_TRACKER_ACTION_FILE,
|
||||
downloadIndex,
|
||||
/* addNewDownloadsAsCompleted= */ true);
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
context,
|
||||
getDatabaseProvider(context),
|
||||
getDownloadCache(context),
|
||||
getHttpDataSourceFactory(context),
|
||||
Executors.newFixedThreadPool(/* nThreads= */ 6));
|
||||
downloadTracker =
|
||||
new DownloadTracker(context, getHttpDataSourceFactory(context), downloadManager);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized void upgradeActionFile(
|
||||
Context context,
|
||||
String fileName,
|
||||
DefaultDownloadIndex downloadIndex,
|
||||
boolean addNewDownloadsAsCompleted) {
|
||||
try {
|
||||
ActionFileUpgradeUtil.upgradeAndDelete(
|
||||
new File(getDownloadDirectory(context), fileName),
|
||||
/* downloadIdProvider= */ null,
|
||||
downloadIndex,
|
||||
/* deleteOnFailure= */ true,
|
||||
addNewDownloadsAsCompleted);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized DatabaseProvider getDatabaseProvider(Context context) {
|
||||
if (databaseProvider == null) {
|
||||
databaseProvider = new StandaloneDatabaseProvider(context);
|
||||
}
|
||||
return databaseProvider;
|
||||
}
|
||||
|
||||
private static synchronized File getDownloadDirectory(Context context) {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getExternalFilesDir(/* type= */ null);
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getFilesDir();
|
||||
}
|
||||
}
|
||||
return downloadDirectory;
|
||||
}
|
||||
|
||||
private static CacheDataSource.Factory buildReadOnlyCacheDataSource(
|
||||
DataSource.Factory upstreamFactory, Cache cache) {
|
||||
return new CacheDataSource.Factory()
|
||||
.setCache(cache)
|
||||
.setUpstreamDataSourceFactory(upstreamFactory)
|
||||
.setCacheWriteDataSinkFactory(null)
|
||||
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
|
||||
}
|
||||
|
||||
private DemoUtil() {}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -21,7 +21,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer2.ui.StyledPlayerView android:id="@+id/player_view"
|
||||
<androidx.media3.ui.StyledPlayerView android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:show_shuffle_button="true"
|
||||
|
8
demos/session/README.md
Normal file
8
demos/session/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Media session demo
|
||||
|
||||
This app demonstrates use of media sessions. It's a reference app written in
|
||||
Kotlin, which demonstrates best practices for media apps that want to advertise
|
||||
media sessions.
|
||||
|
||||
See the [demos README](../README.md) for instructions on how to build and run
|
||||
this demo.
|
69
demos/session/build.gradle
Normal file
69
demos/session/build.gradle
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright 2021 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'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles = [
|
||||
'proguard-rules.txt',
|
||||
getDefaultProguardFile('proguard-android-optimize.txt')
|
||||
]
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// The demo app isn't indexed, and doesn't have translations.
|
||||
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:' + androidxCoreVersion
|
||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation project(modulePrefix + ':lib-exoplayer')
|
||||
implementation project(modulePrefix + ':lib-exoplayer-dash')
|
||||
implementation project(modulePrefix + ':lib-exoplayer-hls')
|
||||
implementation project(modulePrefix + ':lib-ui')
|
||||
implementation project(modulePrefix + ':lib-session')
|
||||
}
|
2
demos/session/proguard-rules.txt
Normal file
2
demos/session/proguard-rules.txt
Normal file
@ -0,0 +1,2 @@
|
||||
# Proguard rules specific to the media3 session demo app.
|
||||
|
59
demos/session/src/main/AndroidManifest.xml
Normal file
59
demos/session/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2021 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="androidx.media3.demo.session">
|
||||
|
||||
<uses-sdk/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Media3Demo">
|
||||
|
||||
<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>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".PlayerActivity"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".PlayableFolderActivity"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".PlaybackService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
491
demos/session/src/main/assets/catalog.json
Normal file
491
demos/session/src/main/assets/catalog.json
Normal file
@ -0,0 +1,491 @@
|
||||
{
|
||||
"media": [
|
||||
{
|
||||
"id": "video_01",
|
||||
"title": "Future Scenerio",
|
||||
"album": "Mango Open Movie project",
|
||||
"artist": "Blender Foundation",
|
||||
"genre": "Video",
|
||||
"source": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
||||
"image": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
|
||||
},
|
||||
{
|
||||
"id": "video_02",
|
||||
"title": "TTML Netflix Japanese examples (IMSC1.1)",
|
||||
"source": "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/ttml/netflix_japanese_ttml.xml",
|
||||
"album": "Video with subtitle",
|
||||
"artist": "Netflix",
|
||||
"genre": "Video",
|
||||
"image": "https://cdn.pixabay.com/photo/2014/10/09/13/14/video-481821_960_720.png"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_01",
|
||||
"title": "Intro - The Way Of Waking Up (feat. Alan Watts)",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/01_-_Intro_-_The_Way_Of_Waking_Up_feat_Alan_Watts.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 1,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 90,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_02",
|
||||
"title": "Geisha",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/02_-_Geisha.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 2,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 267,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_03",
|
||||
"title": "Voyage I - Waterfall",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/03_-_Voyage_I_-_Waterfall.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 3,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 264,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_04",
|
||||
"title": "The Music In You",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/04_-_The_Music_In_You.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 4,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 223,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_05",
|
||||
"title": "The Calm Before The Storm",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/05_-_The_Calm_Before_The_Storm.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 5,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 229,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_06",
|
||||
"title": "No Pain, No Gain",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/06_-_No_Pain_No_Gain.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 6,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 304,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_07",
|
||||
"title": "Voyage II - Satori",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/07_-_Voyage_II_-_Satori.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 7,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 256,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_08",
|
||||
"title": "Reveal the Magic",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/08_-_Reveal_the_Magic.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 8,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 293,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_09",
|
||||
"title": "Hachiko (The Faithtful Dog)",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/09_-_Hachiko_The_Faithtful_Dog.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 9,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 185,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_10",
|
||||
"title": "Wake Up",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/10_-_Wake_Up.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 10,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 251,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_11",
|
||||
"title": "Voyage III - The Space Between Us",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/11_-_Voyage_III_-_The_Space_Between_Us.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 11,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 290,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_12",
|
||||
"title": "Ume No Kaori (feat. Sunawai)",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/12_-_Ume_No_Kaori_feat_Sunawai.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 12,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 334,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "wake_up_13",
|
||||
"title": "Outro - Totally Here and Now (feat. Alan Watts)",
|
||||
"album": "Wake Up",
|
||||
"artist": "The Kyoto Connection",
|
||||
"genre": "Electronic",
|
||||
"source": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/13_-_Outro_-_Totally_Here_and_Now_feat_Alan_Watts.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg",
|
||||
"trackNumber": 13,
|
||||
"totalTrackCount": 13,
|
||||
"duration": 242,
|
||||
"site": "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/"
|
||||
},
|
||||
{
|
||||
"id": "irsens_tale_01",
|
||||
"title": "Intro (.udonthear)",
|
||||
"album": "Irsen's Tale",
|
||||
"artist": "Kai Engel",
|
||||
"genre": "Ambient",
|
||||
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/01_-_Intro_udonthear.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/art.jpg",
|
||||
"trackNumber": 1,
|
||||
"totalTrackCount": 9,
|
||||
"duration": 63,
|
||||
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
|
||||
},
|
||||
{
|
||||
"id": "irsens_tale_02",
|
||||
"title": "Leaving",
|
||||
"album": "Irsen's Tale",
|
||||
"artist": "Kai Engel",
|
||||
"genre": "Ambient",
|
||||
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/02_-_Leaving.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/art.jpg",
|
||||
"trackNumber": 2,
|
||||
"totalTrackCount": 9,
|
||||
"duration": 170,
|
||||
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
|
||||
},
|
||||
{
|
||||
"id": "irsens_tale_03",
|
||||
"title": "Irsen's Tale",
|
||||
"album": "Irsen's Tale",
|
||||
"artist": "Kai Engel",
|
||||
"genre": "Ambient",
|
||||
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/03_-_Irsens_Tale.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/art.jpg",
|
||||
"trackNumber": 3,
|
||||
"totalTrackCount": 9,
|
||||
"duration": 164,
|
||||
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
|
||||
},
|
||||
{
|
||||
"id": "irsens_tale_04",
|
||||
"title": "Moonlight Reprise",
|
||||
"album": "Irsen's Tale",
|
||||
"artist": "Kai Engel",
|
||||
"genre": "Ambient",
|
||||
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/04_-_Moonlight_Reprise.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/art.jpg",
|
||||
"trackNumber": 4,
|
||||
"totalTrackCount": 9,
|
||||
"duration": 181,
|
||||
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
|
||||
},
|
||||
{
|
||||
"id": "irsens_tale_05",
|
||||
"title": "Nothing Lasts Forever",
|
||||
"album": "Irsen's Tale",
|
||||
"artist": "Kai Engel",
|
||||
"genre": "Ambient",
|
||||
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/05_-_Nothing_Lasts_Forever.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/art.jpg",
|
||||
"trackNumber": 5,
|
||||
"totalTrackCount": 9,
|
||||
"duration": 132,
|
||||
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
|
||||
},
|
||||
{
|
||||
"id": "irsens_tale_06",
|
||||
"title": "The Moments of Our Mornings",
|
||||
"album": "Irsen's Tale",
|
||||
"artist": "Kai Engel",
|
||||
"genre": "Ambient",
|
||||
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/06_-_The_Moments_of_Our_Mornings.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/art.jpg",
|
||||
"trackNumber": 6,
|
||||
"totalTrackCount": 9,
|
||||
"duration": 104,
|
||||
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
|
||||
},
|
||||
{
|
||||
"id": "irsens_tale_07",
|
||||
"title": "Laceration",
|
||||
"album": "Irsen's Tale",
|
||||
"artist": "Kai Engel",
|
||||
"genre": "Ambient",
|
||||
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/07_-_Laceration.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/art.jpg",
|
||||
"trackNumber": 7,
|
||||
"totalTrackCount": 9,
|
||||
"duration": 173,
|
||||
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
|
||||
},
|
||||
{
|
||||
"id": "irsens_tale_08",
|
||||
"title": "Memories",
|
||||
"album": "Irsen's Tale",
|
||||
"artist": "Kai Engel",
|
||||
"genre": "Ambient",
|
||||
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/08_-_Memories.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/art.jpg",
|
||||
"trackNumber": 8,
|
||||
"totalTrackCount": 9,
|
||||
"duration": 213,
|
||||
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
|
||||
},
|
||||
{
|
||||
"id": "irsens_tale_09",
|
||||
"title": "Outro",
|
||||
"album": "Irsen's Tale",
|
||||
"artist": "Kai Engel",
|
||||
"genre": "Ambient",
|
||||
"source": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/09_-_Outro.mp3",
|
||||
"image": "https://storage.googleapis.com/uamp/Kai_Engel_-_Irsens_Tale/art.jpg",
|
||||
"trackNumber": 9,
|
||||
"totalTrackCount": 9,
|
||||
"duration": 65,
|
||||
"site": "http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/"
|
||||
},
|
||||
{
|
||||
"id": "jazz_in_paris",
|
||||
"title": "Jazz in Paris",
|
||||
"album": "Jazz & Blues",
|
||||
"artist": "Media Right Productions",
|
||||
"genre": "Jazz & Blues",
|
||||
"source": "https://storage.googleapis.com/automotive-media/Jazz_In_Paris.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art.jpg",
|
||||
"trackNumber": 1,
|
||||
"totalTrackCount": 6,
|
||||
"duration": 103,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "the_messenger",
|
||||
"title": "The Messenger",
|
||||
"album": "Jazz & Blues",
|
||||
"artist": "Silent Partner",
|
||||
"genre": "Jazz & Blues",
|
||||
"source": "https://storage.googleapis.com/automotive-media/The_Messenger.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art.jpg",
|
||||
"trackNumber": 2,
|
||||
"totalTrackCount": 6,
|
||||
"duration": 132,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "talkies",
|
||||
"title": "Talkies",
|
||||
"album": "Jazz & Blues",
|
||||
"artist": "Huma-Huma",
|
||||
"genre": "Jazz & Blues",
|
||||
"source": "https://storage.googleapis.com/automotive-media/Talkies.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art.jpg",
|
||||
"trackNumber": 3,
|
||||
"totalTrackCount": 6,
|
||||
"duration": 162,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "on_the_bach",
|
||||
"title": "On the Bach",
|
||||
"album": "Cinematic",
|
||||
"artist": "Jingle Punks",
|
||||
"genre": "Cinematic",
|
||||
"source": "https://storage.googleapis.com/automotive-media/On_the_Bach.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art.jpg",
|
||||
"trackNumber": 4,
|
||||
"totalTrackCount": 6,
|
||||
"duration": 66,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "the_story_unfolds",
|
||||
"title": "The Story Unfolds",
|
||||
"album": "Cinematic",
|
||||
"artist": "Jingle Punks",
|
||||
"genre": "Cinematic",
|
||||
"source": "https://storage.googleapis.com/automotive-media/The_Story_Unfolds.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art.jpg",
|
||||
"trackNumber": 5,
|
||||
"totalTrackCount": 6,
|
||||
"duration": 91,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "drop_and_roll",
|
||||
"title": "Drop and Roll",
|
||||
"album": "Youtube Audio Library Rock",
|
||||
"artist": "Silent Partner",
|
||||
"genre": "Rock",
|
||||
"source": "https://storage.googleapis.com/automotive-media/Drop_and_Roll.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art_2.jpg",
|
||||
"trackNumber": 1,
|
||||
"totalTrackCount": 7,
|
||||
"duration": 121,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "motocross",
|
||||
"title": "Motocross",
|
||||
"album": "Youtube Audio Library Rock",
|
||||
"artist": "Topher Mohr and Alex Elena",
|
||||
"genre": "Rock",
|
||||
"source": "https://storage.googleapis.com/automotive-media/Motocross.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art_2.jpg",
|
||||
"trackNumber": 2,
|
||||
"totalTrackCount": 7,
|
||||
"duration": 182,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "wish_youd_come_true",
|
||||
"title": "Wish You'd Come True",
|
||||
"album": "Youtube Audio Library Rock",
|
||||
"artist": "The 126ers",
|
||||
"genre": "Rock",
|
||||
"source": "https://storage.googleapis.com/automotive-media/Wish_You_d_Come_True.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art_2.jpg",
|
||||
"trackNumber": 3,
|
||||
"totalTrackCount": 7,
|
||||
"duration": 169,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "awakening",
|
||||
"title": "Awakening",
|
||||
"album": "Youtube Audio Library Rock",
|
||||
"artist": "Silent Partner",
|
||||
"genre": "Rock",
|
||||
"source": "https://storage.googleapis.com/automotive-media/Awakening.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art_2.jpg",
|
||||
"trackNumber": 4,
|
||||
"totalTrackCount": 7,
|
||||
"duration": 220,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "home",
|
||||
"title": "Home",
|
||||
"album": "Youtube Audio Library Rock",
|
||||
"artist": "Letter Box",
|
||||
"genre": "Rock",
|
||||
"source": "https://storage.googleapis.com/automotive-media/Home.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art_2.jpg",
|
||||
"trackNumber": 5,
|
||||
"totalTrackCount": 7,
|
||||
"duration": 213,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "tell_the_angels",
|
||||
"title": "Tell The Angels",
|
||||
"album": "Youtube Audio Library Rock",
|
||||
"artist": "Letter Box",
|
||||
"genre": "Rock",
|
||||
"source": "https://storage.googleapis.com/automotive-media/Tell_The_Angels.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art_2.jpg",
|
||||
"trackNumber": 6,
|
||||
"totalTrackCount": 7,
|
||||
"duration": 208,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "hey_sailor",
|
||||
"title": "Hey Sailor",
|
||||
"album": "Youtube Audio Library Rock",
|
||||
"artist": "Letter Box",
|
||||
"genre": "Rock",
|
||||
"source": "https://storage.googleapis.com/automotive-media/Hey_Sailor.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art_2.jpg",
|
||||
"trackNumber": 7,
|
||||
"totalTrackCount": 7,
|
||||
"duration": 193,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "keys_to_the_kingdom",
|
||||
"title": "Keys To The Kingdom",
|
||||
"album": "Youtube Audio Library Rock 2",
|
||||
"artist": "The 126ers",
|
||||
"genre": "Rock",
|
||||
"source": "https://storage.googleapis.com/automotive-media/Keys_To_The_Kingdom.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art_3.jpg",
|
||||
"trackNumber": 1,
|
||||
"totalTrackCount": 2,
|
||||
"duration": 221,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
},
|
||||
{
|
||||
"id": "the_coldest_shoulder",
|
||||
"title": "The Coldest Shoulder",
|
||||
"album": "Youtube Audio Library Rock 2",
|
||||
"artist": "The 126ers",
|
||||
"genre": "Rock",
|
||||
"source": "https://storage.googleapis.com/automotive-media/The_Coldest_Shoulder.mp3",
|
||||
"image": "https://storage.googleapis.com/automotive-media/album_art_3.jpg",
|
||||
"trackNumber": 2,
|
||||
"totalTrackCount": 2,
|
||||
"duration": 160,
|
||||
"site": "https://www.youtube.com/audiolibrary/music"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright 2021 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 androidx.media3.demo.session
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.session.LibraryResult
|
||||
import androidx.media3.session.MediaBrowser
|
||||
import androidx.media3.session.SessionToken
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
|
||||
private val browser: MediaBrowser?
|
||||
get() = if (browserFuture.isDone) browserFuture.get() else null
|
||||
|
||||
private lateinit var mediaListAdapter: FolderMediaItemArrayAdapter
|
||||
private lateinit var mediaListView: ListView
|
||||
private val treePathStack: ArrayDeque<MediaItem> = ArrayDeque()
|
||||
private var subItemMediaList: MutableList<MediaItem> = mutableListOf()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// setting up the layout
|
||||
setContentView(R.layout.activity_main)
|
||||
mediaListView = findViewById(R.id.media_list_view)
|
||||
mediaListAdapter = FolderMediaItemArrayAdapter(this, R.layout.folder_items, subItemMediaList)
|
||||
mediaListView.adapter = mediaListAdapter
|
||||
|
||||
// setting up on click. When user click on an item, try to display it
|
||||
mediaListView.setOnItemClickListener { _, _, position, _ ->
|
||||
run {
|
||||
val selectedMediaItem = mediaListAdapter.getItem(position)!!
|
||||
// TODO(b/192235359): handle the case where the item is playable but it is not a folder
|
||||
if (selectedMediaItem.mediaMetadata.isPlayable == true) {
|
||||
val intent = PlayableFolderActivity.createIntent(this, selectedMediaItem.mediaId)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
pushPathStack(selectedMediaItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<ExtendedFloatingActionButton>(R.id.open_player_floating_button)
|
||||
.setOnClickListener {
|
||||
// display the playing media items
|
||||
val intent = Intent(this, PlayerActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
popPathStack()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
initializeBrowser()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
releaseBrowser()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
private fun initializeBrowser() {
|
||||
browserFuture =
|
||||
MediaBrowser.Builder(
|
||||
this,
|
||||
SessionToken(this, ComponentName(this, PlaybackService::class.java))
|
||||
)
|
||||
.buildAsync()
|
||||
browserFuture.addListener({ pushRoot() }, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
private fun releaseBrowser() {
|
||||
MediaBrowser.releaseFuture(browserFuture)
|
||||
}
|
||||
|
||||
private fun displayChildrenList(mediaItem: MediaItem) {
|
||||
val browser = this.browser ?: return
|
||||
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(treePathStack.size != 1)
|
||||
val childrenFuture =
|
||||
browser.getChildren(
|
||||
mediaItem.mediaId,
|
||||
/* page= */ 0,
|
||||
/* pageSize= */ Int.MAX_VALUE,
|
||||
/* params= */ null
|
||||
)
|
||||
|
||||
subItemMediaList.clear()
|
||||
childrenFuture.addListener(
|
||||
{
|
||||
val result = childrenFuture.get()!!
|
||||
val children = result.value!!
|
||||
subItemMediaList.addAll(children)
|
||||
mediaListAdapter.notifyDataSetChanged()
|
||||
},
|
||||
MoreExecutors.directExecutor()
|
||||
)
|
||||
}
|
||||
|
||||
private fun pushPathStack(mediaItem: MediaItem) {
|
||||
treePathStack.addLast(mediaItem)
|
||||
displayChildrenList(treePathStack.last())
|
||||
}
|
||||
|
||||
private fun popPathStack() {
|
||||
treePathStack.removeLast()
|
||||
if (treePathStack.size == 0) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
displayChildrenList(treePathStack.last())
|
||||
}
|
||||
|
||||
private fun pushRoot() {
|
||||
// browser can be initialized many times
|
||||
// only push root at the first initialization
|
||||
if (!treePathStack.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val browser = this.browser ?: return
|
||||
val rootFuture = browser.getLibraryRoot(/* params= */ null)
|
||||
rootFuture.addListener(
|
||||
{
|
||||
val result: LibraryResult<MediaItem> = rootFuture.get()!!
|
||||
val root: MediaItem = result.value!!
|
||||
pushPathStack(root)
|
||||
},
|
||||
MoreExecutors.directExecutor()
|
||||
)
|
||||
}
|
||||
|
||||
private class FolderMediaItemArrayAdapter(
|
||||
context: Context,
|
||||
viewID: Int,
|
||||
mediaItemList: List<MediaItem>
|
||||
) : ArrayAdapter<MediaItem>(context, viewID, mediaItemList) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val mediaItem = getItem(position)!!
|
||||
val returnConvertView =
|
||||
convertView ?: LayoutInflater.from(context).inflate(R.layout.folder_items, parent, false)
|
||||
|
||||
returnConvertView.findViewById<TextView>(R.id.media_item).text = mediaItem.mediaMetadata.title
|
||||
return returnConvertView
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,249 @@
|
||||
/*
|
||||
* Copyright 2021 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 androidx.media3.demo.session
|
||||
|
||||
import android.content.res.AssetManager
|
||||
import android.net.Uri
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
|
||||
import androidx.media3.common.util.Util
|
||||
import com.google.common.collect.ImmutableList
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* A sample media catalog that represents media items as a tree.
|
||||
*
|
||||
* It fetched the data from {@code catalog.json}. The root's children are folders containing media
|
||||
* items from the same album/artist/genre.
|
||||
*
|
||||
* Each app should have their own way of representing the tree. MediaItemTree is used for
|
||||
* demonstration purpose only.
|
||||
*/
|
||||
object MediaItemTree {
|
||||
private var treeNodes: MutableMap<String, MediaItemNode> = mutableMapOf()
|
||||
private var titleMap: MutableMap<String, MediaItemNode> = mutableMapOf()
|
||||
private var isInitialized = false
|
||||
private const val ROOT_ID = "[rootID]"
|
||||
private const val ALBUM_ID = "[albumID]"
|
||||
private const val GENRE_ID = "[genreID]"
|
||||
private const val ARTIST_ID = "[artistID]"
|
||||
private const val ALBUM_PREFIX = "[album]"
|
||||
private const val GENRE_PREFIX = "[genre]"
|
||||
private const val ARTIST_PREFIX = "[artist]"
|
||||
private const val ITEM_PREFIX = "[item]"
|
||||
|
||||
private class MediaItemNode(val item: MediaItem) {
|
||||
private val children: MutableList<MediaItem> = ArrayList()
|
||||
|
||||
fun addChild(childID: String) {
|
||||
this.children.add(treeNodes[childID]!!.item)
|
||||
}
|
||||
|
||||
fun getChildren(): List<MediaItem> {
|
||||
return ImmutableList.copyOf(children)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMediaItem(
|
||||
title: String,
|
||||
mediaId: String,
|
||||
isPlayable: Boolean,
|
||||
@MediaMetadata.FolderType folderType: Int,
|
||||
album: String? = null,
|
||||
artist: String? = null,
|
||||
genre: String? = null,
|
||||
sourceUri: Uri? = null,
|
||||
): MediaItem {
|
||||
// TODO(b/194280027): add artwork
|
||||
val metadata =
|
||||
MediaMetadata.Builder()
|
||||
.setAlbumTitle(album)
|
||||
.setTitle(title)
|
||||
.setArtist(artist)
|
||||
.setGenre(genre)
|
||||
.setFolderType(folderType)
|
||||
.setIsPlayable(isPlayable)
|
||||
.build()
|
||||
return MediaItem.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setMediaMetadata(metadata)
|
||||
.setUri(sourceUri)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun loadJSONFromAsset(assets: AssetManager): String {
|
||||
val buffer = assets.open("catalog.json").use { Util.toByteArray(it) }
|
||||
return String(buffer, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
fun initialize(assets: AssetManager) {
|
||||
if (isInitialized) return
|
||||
isInitialized = true
|
||||
// create root and folders for album/artist/genre.
|
||||
treeNodes[ROOT_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Root Folder",
|
||||
mediaId = ROOT_ID,
|
||||
isPlayable = false,
|
||||
folderType = FOLDER_TYPE_MIXED
|
||||
)
|
||||
)
|
||||
treeNodes[ALBUM_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Album Folder",
|
||||
mediaId = ALBUM_ID,
|
||||
isPlayable = false,
|
||||
folderType = FOLDER_TYPE_MIXED
|
||||
)
|
||||
)
|
||||
treeNodes[ARTIST_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Artist Folder",
|
||||
mediaId = ARTIST_ID,
|
||||
isPlayable = false,
|
||||
folderType = FOLDER_TYPE_MIXED
|
||||
)
|
||||
)
|
||||
treeNodes[GENRE_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Genre Folder",
|
||||
mediaId = GENRE_ID,
|
||||
isPlayable = false,
|
||||
folderType = FOLDER_TYPE_MIXED
|
||||
)
|
||||
)
|
||||
treeNodes[ROOT_ID]!!.addChild(ALBUM_ID)
|
||||
treeNodes[ROOT_ID]!!.addChild(ARTIST_ID)
|
||||
treeNodes[ROOT_ID]!!.addChild(GENRE_ID)
|
||||
|
||||
// Here, parse the json file in asset for media list.
|
||||
// We use a file in asset for demo purpose
|
||||
val jsonObject = JSONObject(loadJSONFromAsset(assets))
|
||||
val mediaList = jsonObject.getJSONArray("media")
|
||||
|
||||
// create subfolder with same artist, album, etc.
|
||||
for (i in 0 until mediaList.length()) {
|
||||
addNodeToTree(mediaList.getJSONObject(i))
|
||||
}
|
||||
}
|
||||
|
||||
private fun addNodeToTree(mediaObject: JSONObject) {
|
||||
|
||||
val id = mediaObject.getString("id")
|
||||
val album = mediaObject.getString("album")
|
||||
val title = mediaObject.getString("title")
|
||||
val artist = mediaObject.getString("artist")
|
||||
val genre = mediaObject.getString("genre")
|
||||
val sourceUri = Uri.parse(mediaObject.getString("source"))
|
||||
// key of such items in tree
|
||||
val idInTree = ITEM_PREFIX + id
|
||||
val albumFolderIdInTree = ALBUM_PREFIX + album
|
||||
val artistFolderIdInTree = ARTIST_PREFIX + artist
|
||||
val genreFolderIdInTree = GENRE_PREFIX + genre
|
||||
|
||||
treeNodes[idInTree] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = title,
|
||||
mediaId = idInTree,
|
||||
isPlayable = true,
|
||||
album = album,
|
||||
artist = artist,
|
||||
genre = genre,
|
||||
sourceUri = sourceUri,
|
||||
folderType = FOLDER_TYPE_NONE
|
||||
)
|
||||
)
|
||||
|
||||
titleMap[title.lowercase()] = treeNodes[idInTree]!!
|
||||
|
||||
if (!treeNodes.containsKey(albumFolderIdInTree)) {
|
||||
treeNodes[albumFolderIdInTree] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = album,
|
||||
mediaId = albumFolderIdInTree,
|
||||
isPlayable = true,
|
||||
folderType = FOLDER_TYPE_PLAYLISTS
|
||||
)
|
||||
)
|
||||
treeNodes[ALBUM_ID]!!.addChild(albumFolderIdInTree)
|
||||
}
|
||||
treeNodes[albumFolderIdInTree]!!.addChild(idInTree)
|
||||
|
||||
// add into artist folder
|
||||
if (!treeNodes.containsKey(artistFolderIdInTree)) {
|
||||
treeNodes[artistFolderIdInTree] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = artist,
|
||||
mediaId = artistFolderIdInTree,
|
||||
isPlayable = true,
|
||||
folderType = FOLDER_TYPE_PLAYLISTS
|
||||
)
|
||||
)
|
||||
treeNodes[ARTIST_ID]!!.addChild(artistFolderIdInTree)
|
||||
}
|
||||
treeNodes[artistFolderIdInTree]!!.addChild(idInTree)
|
||||
|
||||
// add into genre folder
|
||||
if (!treeNodes.containsKey(genreFolderIdInTree)) {
|
||||
treeNodes[genreFolderIdInTree] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = genre,
|
||||
mediaId = genreFolderIdInTree,
|
||||
isPlayable = true,
|
||||
folderType = FOLDER_TYPE_PLAYLISTS
|
||||
)
|
||||
)
|
||||
treeNodes[GENRE_ID]!!.addChild(genreFolderIdInTree)
|
||||
}
|
||||
treeNodes[genreFolderIdInTree]!!.addChild(idInTree)
|
||||
}
|
||||
|
||||
fun getItem(id: String): MediaItem? {
|
||||
return treeNodes[id]?.item
|
||||
}
|
||||
|
||||
fun getRootItem(): MediaItem {
|
||||
return treeNodes[ROOT_ID]!!.item
|
||||
}
|
||||
|
||||
fun getChildren(id: String): List<MediaItem>? {
|
||||
return treeNodes[id]?.getChildren()
|
||||
}
|
||||
|
||||
fun getRandomItem(): MediaItem {
|
||||
var curRoot = getRootItem()
|
||||
while (curRoot.mediaMetadata.folderType != FOLDER_TYPE_NONE) {
|
||||
val children = getChildren(curRoot.mediaId)!!
|
||||
curRoot = children.random()
|
||||
}
|
||||
return curRoot
|
||||
}
|
||||
|
||||
fun getItemFromTitle(title: String): MediaItem? {
|
||||
return titleMap[title]?.item
|
||||
}
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright 2021 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 androidx.media3.demo.session
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.session.MediaBrowser
|
||||
import androidx.media3.session.SessionToken
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
|
||||
class PlayableFolderActivity : AppCompatActivity() {
|
||||
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
|
||||
private val browser: MediaBrowser?
|
||||
get() = if (browserFuture.isDone) browserFuture.get() else null
|
||||
|
||||
private lateinit var mediaList: ListView
|
||||
private lateinit var mediaListAdapter: PlayableMediaItemArrayAdapter
|
||||
private val subItemMediaList: MutableList<MediaItem> = mutableListOf()
|
||||
|
||||
companion object {
|
||||
private const val MEDIA_ITEM_ID_KEY = "MEDIA_ITEM_ID_KEY"
|
||||
fun createIntent(context: Context, mediaItemID: String): Intent {
|
||||
val intent = Intent(context, PlayableFolderActivity::class.java)
|
||||
intent.putExtra(MEDIA_ITEM_ID_KEY, mediaItemID)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_playable_folder)
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
mediaList = findViewById(R.id.media_list_view)
|
||||
mediaListAdapter =
|
||||
PlayableMediaItemArrayAdapter(this, R.layout.playable_items, subItemMediaList)
|
||||
mediaList.adapter = mediaListAdapter
|
||||
mediaList.setOnItemClickListener { _, _, position, _ ->
|
||||
run {
|
||||
val browser = this.browser ?: return@run
|
||||
browser.setMediaItems(subItemMediaList)
|
||||
browser.shuffleModeEnabled = false
|
||||
browser.prepare()
|
||||
browser.seekToDefaultPosition(/* windowIndex= */ position)
|
||||
browser.play()
|
||||
val intent = Intent(this, PlayerActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.shuffle_button).setOnClickListener {
|
||||
val browser = this.browser ?: return@setOnClickListener
|
||||
browser.setMediaItems(subItemMediaList)
|
||||
browser.shuffleModeEnabled = true
|
||||
browser.prepare()
|
||||
browser.play()
|
||||
val intent = Intent(this, PlayerActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.play_button).setOnClickListener {
|
||||
val browser = this.browser ?: return@setOnClickListener
|
||||
browser.setMediaItems(subItemMediaList)
|
||||
browser.shuffleModeEnabled = false
|
||||
browser.prepare()
|
||||
browser.play()
|
||||
val intent = Intent(this, PlayerActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
findViewById<ExtendedFloatingActionButton>(R.id.open_player_floating_button)
|
||||
.setOnClickListener {
|
||||
// display the playing media items
|
||||
val intent = Intent(this, PlayerActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
initializeBrowser()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
releaseBrowser()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun initializeBrowser() {
|
||||
browserFuture =
|
||||
MediaBrowser.Builder(
|
||||
this,
|
||||
SessionToken(this, ComponentName(this, PlaybackService::class.java))
|
||||
)
|
||||
.buildAsync()
|
||||
browserFuture.addListener({ displayFolder() }, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
private fun releaseBrowser() {
|
||||
MediaBrowser.releaseFuture(browserFuture)
|
||||
}
|
||||
|
||||
private fun displayFolder() {
|
||||
val browser = this.browser ?: return
|
||||
val id: String = intent.getStringExtra(MEDIA_ITEM_ID_KEY)!!
|
||||
val mediaItemFuture = browser.getItem(id)
|
||||
val childrenFuture =
|
||||
browser.getChildren(id, /* page= */ 0, /* pageSize= */ Int.MAX_VALUE, /* params= */ null)
|
||||
mediaItemFuture.addListener(
|
||||
{
|
||||
val title: TextView = findViewById(R.id.folder_description)
|
||||
val result = mediaItemFuture.get()!!
|
||||
title.text = result.value!!.mediaMetadata.title
|
||||
},
|
||||
MoreExecutors.directExecutor()
|
||||
)
|
||||
childrenFuture.addListener(
|
||||
{
|
||||
val result = childrenFuture.get()!!
|
||||
val children = result.value!!
|
||||
|
||||
subItemMediaList.clear()
|
||||
subItemMediaList.addAll(children)
|
||||
mediaListAdapter.notifyDataSetChanged()
|
||||
},
|
||||
MoreExecutors.directExecutor()
|
||||
)
|
||||
}
|
||||
|
||||
private inner class PlayableMediaItemArrayAdapter(
|
||||
context: Context,
|
||||
viewID: Int,
|
||||
mediaItemList: List<MediaItem>
|
||||
) : ArrayAdapter<MediaItem>(context, viewID, mediaItemList) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val mediaItem = getItem(position)!!
|
||||
val returnConvertView =
|
||||
convertView ?: LayoutInflater.from(context).inflate(R.layout.playable_items, parent, false)
|
||||
|
||||
returnConvertView.findViewById<TextView>(R.id.media_item).text = mediaItem.mediaMetadata.title
|
||||
|
||||
returnConvertView.findViewById<TextView>(R.id.add_button).setOnClickListener {
|
||||
val browser = this@PlayableFolderActivity.browser ?: return@setOnClickListener
|
||||
browser.addMediaItem(mediaItem)
|
||||
Snackbar.make(
|
||||
findViewById<LinearLayout>(R.id.linear_layout),
|
||||
getString(R.string.added_media_item_format, mediaItem.mediaMetadata.title),
|
||||
BaseTransientBottomBar.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
return returnConvertView
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright 2021 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 androidx.media3.demo.session
|
||||
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.TaskStackBuilder
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.LibraryResult
|
||||
import androidx.media3.session.MediaLibraryService
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.SessionResult
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
|
||||
class PlaybackService : MediaLibraryService() {
|
||||
private lateinit var player: ExoPlayer
|
||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
|
||||
|
||||
companion object {
|
||||
private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch"
|
||||
private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri"
|
||||
}
|
||||
|
||||
private inner class CustomMediaLibrarySessionCallback :
|
||||
MediaLibrarySession.MediaLibrarySessionCallback {
|
||||
override fun onGetLibraryRoot(
|
||||
session: MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
params: LibraryParams?
|
||||
): ListenableFuture<LibraryResult<MediaItem>> {
|
||||
return Futures.immediateFuture(
|
||||
LibraryResult.ofItem(MediaItemTree.getRootItem(), /* params= */ null)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onGetItem(
|
||||
session: MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
mediaId: String
|
||||
): ListenableFuture<LibraryResult<MediaItem>> {
|
||||
val item =
|
||||
MediaItemTree.getItem(mediaId)
|
||||
?: return Futures.immediateFuture(
|
||||
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
|
||||
)
|
||||
return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null))
|
||||
}
|
||||
|
||||
override fun onGetChildren(
|
||||
session: MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
parentId: String,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
params: LibraryParams?
|
||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||
val children =
|
||||
MediaItemTree.getChildren(parentId)
|
||||
?: return Futures.immediateFuture(
|
||||
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
|
||||
)
|
||||
|
||||
return Futures.immediateFuture(LibraryResult.ofItemList(children, params))
|
||||
}
|
||||
|
||||
private fun setMediaItemFromSearchQuery(query: String) {
|
||||
// Only accept query with pattern "play [Title]" or "[Title]"
|
||||
// Where [Title]: must be exactly matched
|
||||
// If no media with exact name found, play a random media instead
|
||||
lateinit var mediaTitle: String
|
||||
if (query.lowercase().startsWith("play ")) {
|
||||
mediaTitle = query.subSequence(5, query.length).toString()
|
||||
} else {
|
||||
mediaTitle = query
|
||||
}
|
||||
|
||||
val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem()
|
||||
player.setMediaItem(item)
|
||||
player.prepare()
|
||||
}
|
||||
|
||||
override fun onSetMediaUri(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
uri: Uri,
|
||||
extras: Bundle
|
||||
): Int {
|
||||
|
||||
if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) ||
|
||||
uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT)
|
||||
) {
|
||||
var searchQuery =
|
||||
uri.getQueryParameter("query") ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED
|
||||
setMediaItemFromSearchQuery(searchQuery)
|
||||
|
||||
return SessionResult.RESULT_SUCCESS
|
||||
} else {
|
||||
return SessionResult.RESULT_ERROR_NOT_SUPPORTED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomMediaItemFiller : MediaSession.MediaItemFiller {
|
||||
override fun fillInLocalConfiguration(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
mediaItem: MediaItem
|
||||
): MediaItem {
|
||||
return MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initializeSessionAndPlayer()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
player.release()
|
||||
mediaLibrarySession.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession {
|
||||
return mediaLibrarySession
|
||||
}
|
||||
|
||||
private fun initializeSessionAndPlayer() {
|
||||
player =
|
||||
ExoPlayer.Builder(this)
|
||||
.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true)
|
||||
.build()
|
||||
MediaItemTree.initialize(assets)
|
||||
|
||||
val parentScreenIntent = Intent(this, MainActivity::class.java)
|
||||
val intent = Intent(this, PlayerActivity::class.java)
|
||||
|
||||
val pendingIntent =
|
||||
TaskStackBuilder.create(this).run {
|
||||
addNextIntent(parentScreenIntent)
|
||||
addNextIntent(intent)
|
||||
|
||||
val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0
|
||||
getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
mediaLibrarySession =
|
||||
MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||
.setMediaItemFiller(CustomMediaItemFiller())
|
||||
.setSessionActivity(pendingIntent)
|
||||
.build()
|
||||
}
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2021 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 androidx.media3.demo.session
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import androidx.media3.ui.StyledPlayerView
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
|
||||
class PlayerActivity : AppCompatActivity() {
|
||||
private lateinit var controllerFuture: ListenableFuture<MediaController>
|
||||
private val controller: MediaController?
|
||||
get() = if (controllerFuture.isDone) controllerFuture.get() else null
|
||||
|
||||
private lateinit var playerView: StyledPlayerView
|
||||
private lateinit var mediaList: ListView
|
||||
private lateinit var mediaListAdapter: PlayingMediaItemArrayAdapter
|
||||
private val subItemMediaList: MutableList<MediaItem> = mutableListOf()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_player)
|
||||
playerView = findViewById(R.id.player_view)
|
||||
|
||||
mediaList = findViewById(R.id.current_playing_list)
|
||||
mediaListAdapter = PlayingMediaItemArrayAdapter(this, R.layout.folder_items, subItemMediaList)
|
||||
mediaList.adapter = mediaListAdapter
|
||||
mediaList.setOnItemClickListener { _, _, position, _ ->
|
||||
run {
|
||||
val controller = this.controller ?: return@run
|
||||
controller.seekToDefaultPosition(/* windowIndex= */ position)
|
||||
mediaListAdapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<ImageView>(R.id.shuffle_switch).setOnClickListener {
|
||||
val controller = this.controller ?: return@setOnClickListener
|
||||
controller.shuffleModeEnabled = !controller.shuffleModeEnabled
|
||||
}
|
||||
|
||||
findViewById<ImageView>(R.id.repeat_switch).setOnClickListener {
|
||||
val controller = this.controller ?: return@setOnClickListener
|
||||
when (controller.repeatMode) {
|
||||
Player.REPEAT_MODE_ALL -> controller.repeatMode = Player.REPEAT_MODE_OFF
|
||||
Player.REPEAT_MODE_OFF -> controller.repeatMode = Player.REPEAT_MODE_ONE
|
||||
Player.REPEAT_MODE_ONE -> controller.repeatMode = Player.REPEAT_MODE_ALL
|
||||
}
|
||||
}
|
||||
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
initializeController()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
playerView.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
playerView.onPause()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
playerView.player = null
|
||||
releaseController()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun initializeController() {
|
||||
controllerFuture =
|
||||
MediaController.Builder(
|
||||
this,
|
||||
SessionToken(this, ComponentName(this, PlaybackService::class.java))
|
||||
)
|
||||
.buildAsync()
|
||||
controllerFuture.addListener({ setController() }, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
private fun releaseController() {
|
||||
MediaController.releaseFuture(controllerFuture)
|
||||
}
|
||||
|
||||
private fun setController() {
|
||||
val controller = this.controller ?: return
|
||||
|
||||
playerView.player = controller
|
||||
|
||||
updateCurrentPlaylistUI()
|
||||
updateMediaMetadataUI(controller.mediaMetadata)
|
||||
updateShuffleSwitchUI(controller.shuffleModeEnabled)
|
||||
updateRepeatSwitchUI(controller.repeatMode)
|
||||
|
||||
controller.addListener(
|
||||
object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
updateMediaMetadataUI(mediaItem?.mediaMetadata ?: MediaMetadata.EMPTY)
|
||||
}
|
||||
|
||||
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
||||
updateShuffleSwitchUI(shuffleModeEnabled)
|
||||
}
|
||||
|
||||
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||
updateRepeatSwitchUI(repeatMode)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateShuffleSwitchUI(shuffleModeEnabled: Boolean) {
|
||||
val resId =
|
||||
if (shuffleModeEnabled) R.drawable.exo_controls_shuffle_on
|
||||
else R.drawable.exo_controls_shuffle_off
|
||||
findViewById<ImageView>(R.id.shuffle_switch)
|
||||
.setImageDrawable(ContextCompat.getDrawable(this, resId))
|
||||
}
|
||||
|
||||
private fun updateRepeatSwitchUI(repeatMode: Int) {
|
||||
var resId: Int
|
||||
when (repeatMode) {
|
||||
Player.REPEAT_MODE_OFF -> resId = R.drawable.exo_controls_repeat_off
|
||||
Player.REPEAT_MODE_ONE -> resId = R.drawable.exo_controls_repeat_one
|
||||
Player.REPEAT_MODE_ALL -> resId = R.drawable.exo_controls_repeat_all
|
||||
else -> resId = R.drawable.exo_icon_repeat_off
|
||||
}
|
||||
findViewById<ImageView>(R.id.repeat_switch)
|
||||
.setImageDrawable(ContextCompat.getDrawable(this, resId))
|
||||
}
|
||||
|
||||
private fun updateMediaMetadataUI(mediaMetadata: MediaMetadata) {
|
||||
val title: CharSequence = mediaMetadata.title ?: getString(R.string.no_item_prompt)
|
||||
|
||||
findViewById<TextView>(R.id.video_title).text = title
|
||||
findViewById<TextView>(R.id.video_album).text = mediaMetadata.albumTitle
|
||||
findViewById<TextView>(R.id.video_artist).text = mediaMetadata.artist
|
||||
findViewById<TextView>(R.id.video_genre).text = mediaMetadata.genre
|
||||
|
||||
// Trick to update playlist UI
|
||||
mediaListAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun updateCurrentPlaylistUI() {
|
||||
val controller = this.controller ?: return
|
||||
subItemMediaList.clear()
|
||||
for (i in 0 until controller.mediaItemCount) {
|
||||
subItemMediaList.add(controller.getMediaItemAt(i))
|
||||
}
|
||||
mediaListAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private inner class PlayingMediaItemArrayAdapter(
|
||||
context: Context,
|
||||
viewID: Int,
|
||||
mediaItemList: List<MediaItem>
|
||||
) : ArrayAdapter<MediaItem>(context, viewID, mediaItemList) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val mediaItem = getItem(position)!!
|
||||
val returnConvertView =
|
||||
convertView ?: LayoutInflater.from(context).inflate(R.layout.playlist_items, parent, false)
|
||||
|
||||
returnConvertView.findViewById<TextView>(R.id.media_item).text = mediaItem.mediaMetadata.title
|
||||
|
||||
if (position == controller?.currentWindowIndex) {
|
||||
returnConvertView.setBackgroundColor(ContextCompat.getColor(context, R.color.white))
|
||||
returnConvertView
|
||||
.findViewById<TextView>(R.id.media_item)
|
||||
.setTextColor(ContextCompat.getColor(context, R.color.black))
|
||||
} else {
|
||||
returnConvertView.setBackgroundColor(ContextCompat.getColor(context, R.color.black))
|
||||
returnConvertView
|
||||
.findViewById<TextView>(R.id.media_item)
|
||||
.setTextColor(ContextCompat.getColor(context, R.color.white))
|
||||
}
|
||||
|
||||
returnConvertView.findViewById<Button>(R.id.delete_button).setOnClickListener {
|
||||
val controller = this@PlayerActivity.controller ?: return@setOnClickListener
|
||||
controller.removeMediaItem(position)
|
||||
updateCurrentPlaylistUI()
|
||||
}
|
||||
|
||||
return returnConvertView
|
||||
}
|
||||
}
|
||||
}
|
19
demos/session/src/main/res/drawable/divider.xml
Normal file
19
demos/session/src/main/res/drawable/divider.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2021 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.
|
||||
-->
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient android:startColor="@color/grey" android:endColor="@color/grey"/>
|
||||
</shape>
|
42
demos/session/src/main/res/layout/activity_main.xml
Normal file
42
demos/session/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2021 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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/media_list_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
</ListView>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/open_player_floating_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:textColor="@color/white"
|
||||
android:layout_margin="10dp"
|
||||
app:icon="@drawable/exo_icon_play"
|
||||
app:iconTint="@color/white"
|
||||
app:backgroundTint="@color/purple_500"
|
||||
android:text="@string/current_playlist_name"
|
||||
android:contentDescription="@string/open_player_content_description" />
|
||||
|
||||
</FrameLayout>
|
@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2021 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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/linear_layout"
|
||||
tools:context=".MainActivity">
|
||||
<TextView
|
||||
android:id="@+id/folder_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/current_playlist_name"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Display3" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/shuffle_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_margin="10dp"
|
||||
android:textColor="@color/white"
|
||||
android:drawableLeft="@drawable/exo_controls_shuffle_on"
|
||||
android:text="@string/shuffle" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/play_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_margin="10dp"
|
||||
android:drawableLeft="@drawable/exo_icon_play"
|
||||
android:text="@string/play_button" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/media_list_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
</ListView>
|
||||
|
||||
</LinearLayout>
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/open_player_floating_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:textColor="@color/white"
|
||||
android:layout_margin="10dp"
|
||||
app:icon="@drawable/exo_icon_play"
|
||||
app:iconTint="@color/white"
|
||||
app:backgroundTint="@color/purple_500"
|
||||
android:text="@string/current_playlist_name"
|
||||
android:contentDescription="@string/open_player_content_description" />
|
||||
</FrameLayout>
|
104
demos/session/src/main/res/layout/activity_player.xml
Normal file
104
demos/session/src/main/res/layout/activity_player.xml
Normal file
@ -0,0 +1,104 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2021 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
tools:context=".PlayerActivity">
|
||||
|
||||
<androidx.media3.ui.AspectRatioFrameLayout
|
||||
android:layout_height="300dp"
|
||||
android:layout_width="match_parent"
|
||||
>
|
||||
<androidx.media3.ui.StyledPlayerView
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:use_artwork="true" />
|
||||
</androidx.media3.ui.AspectRatioFrameLayout>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_album"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingBottom="10dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_genre"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:gravity="center_vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/shuffle_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/shuffle"
|
||||
android:src="@drawable/exo_controls_shuffle_off"
|
||||
android:textColor="@color/white" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:layout_margin="@dimen/exo_icon_horizontal_margin"
|
||||
android:id="@+id/repeat_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/exo_controls_repeat_off"
|
||||
android:textColor="@color/white"
|
||||
android:contentDescription="@string/repeat"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/current_playing_list"
|
||||
android:layout_width="match_parent"
|
||||
android:divider="@drawable/divider"
|
||||
android:dividerHeight="1px"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
31
demos/session/src/main/res/layout/folder_items.xml
Normal file
31
demos/session/src/main/res/layout/folder_items.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2021 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:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/media_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="5"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:minHeight="50dp" />
|
||||
|
||||
</LinearLayout>
|
40
demos/session/src/main/res/layout/playable_items.xml
Normal file
40
demos/session/src/main/res/layout/playable_items.xml
Normal file
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2021 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:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/media_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="5"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:minHeight="50dp" />
|
||||
|
||||
<Button
|
||||
android:focusable="false"
|
||||
android:focusableInTouchMode="false"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/add_button"
|
||||
android:background="@android:drawable/ic_input_add"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
41
demos/session/src/main/res/layout/playlist_items.xml
Normal file
41
demos/session/src/main/res/layout/playlist_items.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2021 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:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/media_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="5"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="10dp"
|
||||
android:textColor="@color/white"
|
||||
android:paddingEnd="10dp"
|
||||
android:minHeight="50dp" />
|
||||
|
||||
<Button
|
||||
android:focusable="false"
|
||||
android:focusableInTouchMode="false"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/delete_button"
|
||||
android:background="@android:drawable/ic_menu_close_clear_cancel"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
BIN
demos/session/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
demos/session/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
demos/session/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
demos/session/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
demos/session/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
demos/session/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
BIN
demos/session/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
demos/session/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
BIN
demos/session/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
demos/session/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
33
demos/session/src/main/res/values-night/themes.xml
Normal file
33
demos/session/src/main/res/values-night/themes.xml
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2021 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Media3Demo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">
|
||||
?attr/colorPrimaryVariant
|
||||
</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
25
demos/session/src/main/res/values/colors.xml
Normal file
25
demos/session/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2021 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="grey">#FF999999</color>
|
||||
</resources>
|
27
demos/session/src/main/res/values/strings.xml
Normal file
27
demos/session/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2021 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">Media3 Session Demo</string>
|
||||
<string name="current_playlist_name">Current playlist</string>
|
||||
<string name="open_player_content_description">Click to view your play list</string>
|
||||
<string name="added_media_item_format">Added %1$s to playlist</string>
|
||||
<string name="shuffle">Shuffle</string>
|
||||
<string name="repeat">Repeat</string>
|
||||
<string name="play_button">Play</string>
|
||||
<string name="no_item_prompt">
|
||||
"! No media in the play list !\nPlease try to add more from browser"
|
||||
</string>
|
||||
</resources>
|
33
demos/session/src/main/res/values/themes.xml
Normal file
33
demos/session/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2021 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Media3Demo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">
|
||||
?attr/colorPrimaryVariant
|
||||
</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
@ -45,11 +45,11 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-rtsp')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'lib-exoplayer')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-dash')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-hls')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-rtsp')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-smoothstreaming')
|
||||
implementation project(modulePrefix + 'lib-ui')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.surfacedemo">
|
||||
package="androidx.media3.demo.surface">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
@ -33,7 +33,7 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.surfacedemo.action.VIEW"/>
|
||||
<action android:name="androidx.media3.demo.surface.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
|
@ -0,0 +1,280 @@
|
||||
/*
|
||||
* 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 androidx.media3.demo.surface;
|
||||
|
||||
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 androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.DefaultDataSource;
|
||||
import androidx.media3.datasource.DefaultHttpDataSource;
|
||||
import androidx.media3.datasource.HttpDataSource;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.dash.DashMediaSource;
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager;
|
||||
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
||||
import androidx.media3.exoplayer.drm.FrameworkMediaDrm;
|
||||
import androidx.media3.exoplayer.drm.HttpMediaDrmCallback;
|
||||
import androidx.media3.exoplayer.source.MediaSource;
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
|
||||
import androidx.media3.ui.PlayerControlView;
|
||||
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 = "androidx.media3.demo.surface.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 ExoPlayer 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);
|
||||
DrmSessionManager 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 DefaultHttpDataSource.Factory();
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
|
||||
drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.build(drmCallback);
|
||||
} else {
|
||||
drmSessionManager = DrmSessionManager.DRM_UNSUPPORTED;
|
||||
}
|
||||
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this);
|
||||
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(MediaItem.fromUri(uri));
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
ExoPlayer player = new ExoPlayer.Builder(getApplicationContext()).build();
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package androidx.media3.demo.surface;
|
||||
|
||||
import androidx.media3.common.util.NonNullApi;
|
@ -1,280 +0,0 @@
|
||||
/*
|
||||
* 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.ExoPlayer;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
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.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||
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 ExoPlayer 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);
|
||||
DrmSessionManager 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 DefaultHttpDataSource.Factory();
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
|
||||
drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.build(drmCallback);
|
||||
} else {
|
||||
drmSessionManager = DrmSessionManager.DRM_UNSUPPORTED;
|
||||
}
|
||||
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this);
|
||||
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(MediaItem.fromUri(uri));
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
ExoPlayer player = new ExoPlayer.Builder(getApplicationContext()).build();
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.surfacedemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -32,7 +32,7 @@
|
||||
android:layout_weight="1"
|
||||
android:columnCount="3"/>
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerControlView
|
||||
<androidx.media3.ui.PlayerControlView
|
||||
android:id="@+id/player_control_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -1,9 +0,0 @@
|
||||
# Mercurial's .hgignore files can only be used in the root directory.
|
||||
# You can still apply these rules by adding
|
||||
# include:path/to/this/directory/.hgignore to the top-level .hgignore file.
|
||||
|
||||
# Ensure same syntax as in .gitignore can be used
|
||||
syntax:glob
|
||||
|
||||
_site
|
||||
Gemfile.lock
|
@ -1,3 +0,0 @@
|
||||
---
|
||||
layout: 404
|
||||
---
|
@ -1 +0,0 @@
|
||||
exoplayer.dev
|
@ -1,6 +0,0 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "github-pages", group: :jekyll_plugins
|
||||
|
||||
gem "tzinfo-data"
|
||||
gem "wdm", "~> 0.1.0" if Gem.win_platform?
|
23
docs/LICENSE
23
docs/LICENSE
@ -1,23 +0,0 @@
|
||||
Jekyll TeXt Theme
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Tian Qi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -1,12 +0,0 @@
|
||||
# ExoPlayer website
|
||||
|
||||
The [ExoPlayer website](https://exoplayer.dev/) is hosted on
|
||||
GitHub Pages, and is statically generated using Jekyll.
|
||||
|
||||
* GitHub provides a guide describing how to setup a GitHub Pages site using
|
||||
Jekyll
|
||||
[here](https://help.github.com/articles/using-jekyll-as-a-static-site-generator-with-github-pages/).
|
||||
* GitHub provides a guide describing how to test changes to the site locally
|
||||
[here](https://help.github.com/articles/setting-up-your-github-pages-site-locally-with-jekyll/).
|
||||
Once your machine is setup, you can build and run a local instance of the
|
||||
site using `./run_locally.sh` from the root directory.
|
@ -1,95 +0,0 @@
|
||||
## => Site Settings
|
||||
##############################
|
||||
text_skin: default # "default" (default), "dark", "forest", "ocean", "chocolate", "orange"
|
||||
highlight_theme: default # "default" (default), "tomorrow", "tomorrow-night", "tomorrow-night-eighties", "tomorrow-night-blue", "tomorrow-night-bright"
|
||||
url: https://exoplayer.dev
|
||||
baseurl:
|
||||
title: ExoPlayer
|
||||
description: An application level media player for Android
|
||||
|
||||
|
||||
## => Link roots
|
||||
##############################
|
||||
release_v2: https://github.com/google/ExoPlayer/tree/release-v2
|
||||
exo_sdk: /doc/reference/com/google/android/exoplayer2
|
||||
android_sdk: https://developer.android.com/reference
|
||||
google_sdk: https://developers.google.com
|
||||
|
||||
|
||||
## => GitHub Repository (if the site is hosted by GitHub)
|
||||
##############################
|
||||
repository: google/ExoPlayer
|
||||
repository_tree: gh-pages
|
||||
|
||||
|
||||
## => Author and Social
|
||||
##############################
|
||||
author:
|
||||
type : "organization"
|
||||
name : "ExoPlayer"
|
||||
github : "google/ExoPlayer"
|
||||
medium : "google-exoplayer"
|
||||
|
||||
## => Paths
|
||||
##############################
|
||||
paths:
|
||||
root : # title link url, "/" (default)
|
||||
home : # home layout url, "/" (default)
|
||||
archive : # "/archive.html" (default)
|
||||
rss : # "/feed.xml" (default)
|
||||
|
||||
|
||||
## => Post
|
||||
##############################
|
||||
excerpt_separator: <!--more-->
|
||||
|
||||
|
||||
## => Analytics
|
||||
##############################
|
||||
analytics:
|
||||
provider: google
|
||||
google:
|
||||
tracking_id : UA-68257324-1
|
||||
anonymize_ip: true
|
||||
|
||||
|
||||
## => Search
|
||||
##############################
|
||||
search:
|
||||
provider: google # "default" (default), false, "google", "custom"
|
||||
|
||||
## Google Custom Search Engine
|
||||
google:
|
||||
custom_search_engine_id: 009764199895742571316:nuhckqry2_e
|
||||
|
||||
|
||||
## => Build
|
||||
##############################
|
||||
markdown : kramdown
|
||||
highlighter : rouge
|
||||
permalink : date
|
||||
|
||||
exclude:
|
||||
- Gemfile
|
||||
- README.md
|
||||
- vendor
|
||||
|
||||
defaults:
|
||||
- scope:
|
||||
path: ""
|
||||
values:
|
||||
footer: true
|
||||
show_date: false
|
||||
layout: article
|
||||
sidebar:
|
||||
nav: en
|
||||
|
||||
|
||||
## => Plugins
|
||||
##############################
|
||||
plugins:
|
||||
- jekyll-feed
|
||||
- jekyll-paginate
|
||||
- jekyll-sitemap
|
||||
- jekyll-redirect-from
|
||||
- jemoji
|
@ -1,111 +0,0 @@
|
||||
## => English
|
||||
########################
|
||||
en: &EN
|
||||
SUBSCRIBE : "Subscribe"
|
||||
READMORE : "Read more"
|
||||
SEARCH : "Search"
|
||||
CANCEL : "Cancel"
|
||||
VIEWS : "views"
|
||||
LAST_UPDATED : "Last updated"
|
||||
PREVIOUS : "PREVIOUS"
|
||||
NEXT : "NEXT"
|
||||
ARTICLE_DATE_FORMAT : "%b %d, %Y"
|
||||
ARTICLE_LIST_DATE_FORMAT: "%b %d"
|
||||
STATISTICS : "[POST_COUNT] post articles, [PAGE_COUNT] pages."
|
||||
LICENSE_ANNOUNCE : "This work is licensed under a [LICENSE] license."
|
||||
POST_ON_GITHUB : "Edit on Github"
|
||||
FOLLOW_ME : "Follow me on [NAME]."
|
||||
FOLLOW_US : "Follow us on [NAME]."
|
||||
EMAIL_ME : "Send me Email."
|
||||
EMAIL_US : "Send us Email."
|
||||
COPYRIGHT_DATES : "2019"
|
||||
|
||||
en-GB:
|
||||
<<: *EN
|
||||
en-US:
|
||||
<<: *EN
|
||||
en-CA:
|
||||
<<: *EN
|
||||
en-AU:
|
||||
<<: *EN
|
||||
|
||||
## => Simplified Chinese
|
||||
########################
|
||||
zh-Hans: &ZH_HANS
|
||||
SUBSCRIBE : "订阅"
|
||||
READMORE : "阅读更多"
|
||||
SEARCH : "搜索"
|
||||
CANCEL : "取消"
|
||||
VIEWS : "阅读"
|
||||
LAST_UPDATED : "更新于"
|
||||
PREVIOUS : "上篇"
|
||||
NEXT : "下篇"
|
||||
ARTICLE_DATE_FORMAT : "%Y年 %m月%d日"
|
||||
ARTICLE_LIST_DATE_FORMAT: "%m月%d日"
|
||||
STATISTICS : "共计 [POST_COUNT] 篇文章,[PAGE_COUNT] 页。"
|
||||
LICENSE_ANNOUNCE : "本文遵守 [LICENSE] 许可协议。"
|
||||
POST_ON_GITHUB : "在 Github 上修改"
|
||||
FOLLOW_ME : "在 [NAME] 上关注我。"
|
||||
FOLLOW_US : "在 [NAME] 上关注我们。"
|
||||
EMAIL_ME : "给我发邮件。"
|
||||
EMAIL_US : "给我们发邮件。"
|
||||
COPYRIGHT_DATES : "2019"
|
||||
|
||||
zh:
|
||||
<<: *ZH_HANS
|
||||
zh-CN:
|
||||
<<: *ZH_HANS
|
||||
zh-SG:
|
||||
<<: *ZH_HANS
|
||||
|
||||
## => Traditional Chinese
|
||||
########################
|
||||
zh-Hant: &ZH_HANT
|
||||
SUBSCRIBE : "訂閱"
|
||||
READMORE : "閱讀更多"
|
||||
SEARCH : "搜索"
|
||||
CANCEL : "取消"
|
||||
VIEWS : "閱讀"
|
||||
LAST_UPDATED : "更新於"
|
||||
PREVIOUS : "上篇"
|
||||
NEXT : "下篇"
|
||||
ARTICLE_DATE_FORMAT : "%Y年 %m月%d日"
|
||||
ARTICLE_LIST_DATE_FORMAT: "%m月%d日"
|
||||
STATISTICS : "共計 [POST_COUNT] 篇文章,[PAGE_COUNT] 頁。"
|
||||
LICENSE_ANNOUNCE : "本文遵守 [LICENSE] 許可協議。"
|
||||
POST_ON_GITHUB : "在 Github 上修改"
|
||||
FOLLOW_ME : "在 [NAME] 上關注我。"
|
||||
FOLLOW_US : "在 [NAME] 上關注我們。"
|
||||
EMAIL_ME : "給我發郵件。"
|
||||
EMAIL_US : "給我們發郵件。"
|
||||
COPYRIGHT_DATES : "2019"
|
||||
|
||||
zh-TW:
|
||||
<<: *ZH_HANT
|
||||
zh-HK:
|
||||
<<: *ZH_HANT
|
||||
|
||||
## => Korean
|
||||
########################
|
||||
ko: &KO
|
||||
SUBSCRIBE : "구독하기"
|
||||
READMORE : "더보기"
|
||||
SEARCH : "검색"
|
||||
CANCEL : "취소"
|
||||
VIEWS : "조회"
|
||||
LAST_UPDATED : "마지막 수정"
|
||||
PREVIOUS : "이전"
|
||||
NEXT : "다음"
|
||||
ARTICLE_DATE_FORMAT : "%Y년 %m월 %d일"
|
||||
ARTICLE_LIST_DATE_FORMAT: "%m월 %d일"
|
||||
STATISTICS : "전체 글 [POST_COUNT]개, [PAGE_COUNT] 페이지"
|
||||
LICENSE_ANNOUNCE : "이 글의 저작권은 [LICENSE] 라이센스를 따릅니다."
|
||||
POST_ON_GITHUB : "Github에서 확인하기"
|
||||
FOLLOW_ME : "[NAME]에서 팔로우하기"
|
||||
FOLLOW_US : "[NAME]에서 팔로우하기"
|
||||
EMAIL_ME : "이메일 보내기"
|
||||
EMAIL_US : "이메일 보내기"
|
||||
COPYRIGHT_DATES : "2019"
|
||||
|
||||
ko-KR:
|
||||
<<: *KO
|
@ -1,83 +0,0 @@
|
||||
header:
|
||||
- title: Javadoc
|
||||
url: doc/reference/
|
||||
- title: GitHub
|
||||
url: https://github.com/google/ExoPlayer
|
||||
- title: Blog
|
||||
url: https://medium.com/google-exoplayer
|
||||
|
||||
en:
|
||||
- title:
|
||||
children:
|
||||
- title: Home
|
||||
url: index.html
|
||||
- title: Pros and cons
|
||||
url: pros-and-cons.html
|
||||
- title: Demo application
|
||||
url: demo-application.html
|
||||
- title: Supported formats
|
||||
url: supported-formats.html
|
||||
- title: Supported devices
|
||||
url: supported-devices.html
|
||||
- title: Glossary
|
||||
url: glossary.html
|
||||
- title: Getting started
|
||||
children:
|
||||
- title: Hello world
|
||||
url: hello-world.html
|
||||
- title: Player events
|
||||
url: listening-to-player-events.html
|
||||
- title: Playlists
|
||||
url: playlists.html
|
||||
- title: Media items
|
||||
url: media-items.html
|
||||
- title: Media sources
|
||||
url: media-sources.html
|
||||
- title: Track selection
|
||||
url: track-selection.html
|
||||
- title: UI components
|
||||
url: ui-components.html
|
||||
- title: Downloading media
|
||||
url: downloading-media.html
|
||||
- title: Ad insertion
|
||||
url: ad-insertion.html
|
||||
- title: Retrieving metadata
|
||||
url: retrieving-metadata.html
|
||||
- title: Live streaming
|
||||
url: live-streaming.html
|
||||
- title: Network stacks
|
||||
url: network-stacks.html
|
||||
- title: Debug logging
|
||||
url: debug-logging.html
|
||||
- title: Analytics
|
||||
url: analytics.html
|
||||
- title: Media types
|
||||
children:
|
||||
- title: DASH
|
||||
url: dash.html
|
||||
- title: HLS
|
||||
url: hls.html
|
||||
- title: SmoothStreaming
|
||||
url: smoothstreaming.html
|
||||
- title: Progressive
|
||||
url: progressive.html
|
||||
- title: RTSP
|
||||
url: rtsp.html
|
||||
- title: Advanced topics
|
||||
children:
|
||||
- title: Digital rights management
|
||||
url: drm.html
|
||||
- title: Troubleshooting
|
||||
url: troubleshooting.html
|
||||
- title: Customization
|
||||
url: customization.html
|
||||
- title: Transforming media
|
||||
url: transforming-media.html
|
||||
- title: Battery consumption
|
||||
url: battery-consumption.html
|
||||
- title: APK shrinking
|
||||
url: shrinking.html
|
||||
- title: OEM testing
|
||||
url: oems.html
|
||||
- title: Design documents
|
||||
url: design-documents.html
|
@ -1,64 +0,0 @@
|
||||
default:
|
||||
text_skin: default
|
||||
highlight_theme: default
|
||||
lang: en
|
||||
paths:
|
||||
root: /
|
||||
home: /
|
||||
archive: /archive.html
|
||||
rss: /feed.xml
|
||||
mathjax: false
|
||||
mathjax_autoNumber: false
|
||||
mermaid: false
|
||||
chart: false
|
||||
toc:
|
||||
selectors: 'h1,h2,h3'
|
||||
sources: bootcdn
|
||||
|
||||
page:
|
||||
mode: normal
|
||||
type: webpage
|
||||
article_header:
|
||||
align: left
|
||||
theme: light
|
||||
articles:
|
||||
show_cover: true
|
||||
show_excerpt: false
|
||||
show_readmore: false
|
||||
show_info: false
|
||||
show_title: true
|
||||
show_edit_on_github: false
|
||||
show_date: true
|
||||
show_tags: true
|
||||
show_author_profile: false
|
||||
show_subscribe: false
|
||||
full_width: false
|
||||
sharing: false
|
||||
comment: true
|
||||
license: false
|
||||
pageview: false
|
||||
search: default
|
||||
|
||||
sources:
|
||||
bootcdn:
|
||||
font_awesome: 'https://use.fontawesome.com/releases/v5.0.13/css/all.css'
|
||||
jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js'
|
||||
leancloud_js_sdk: '//cdn1.lncld.net/static/js/3.4.1/av-min.js'
|
||||
chart: 'https://cdn.bootcss.com/Chart.js/2.7.2/Chart.bundle.min.js'
|
||||
gitalk:
|
||||
js: 'https://cdn.bootcss.com/gitalk/1.2.2/gitalk.min.js'
|
||||
css: 'https://cdn.bootcss.com/gitalk/1.2.2/gitalk.min.css'
|
||||
valine: 'https://unpkg.com/valine/dist/Valine.min.js' # bootcdn not available
|
||||
mathjax: 'https://cdn.bootcss.com/mathjax/2.7.4/MathJax.js?config=TeX-MML-AM_CHTML'
|
||||
mermaid: 'https://cdn.bootcss.com/mermaid/8.0.0-rc.8/mermaid.min.js'
|
||||
unpkg:
|
||||
font_awesome: 'https://use.fontawesome.com/releases/v5.0.13/css/all.css'
|
||||
jquery: 'https://unpkg.com/jquery@3.3.1/dist/jquery.min.js'
|
||||
leancloud_js_sdk: '//cdn1.lncld.net/static/js/3.4.1/av-min.js'
|
||||
chart: 'https://unpkg.com/chart.js@2.7.2/dist/Chart.min.js'
|
||||
gitalk:
|
||||
js: 'https://unpkg.com/gitalk@1.2.2/dist/gitalk.min.js'
|
||||
css: 'https://unpkg.com/gitalk@1.2.2/dist/gitalk.css'
|
||||
valine: 'https//unpkg.com/valine/dist/Valine.min.js'
|
||||
mathjax: 'https://unpkg.com/mathjax@2.7.4/unpacked/MathJax.js?config=TeX-MML-AM_CHTML'
|
||||
mermaid: 'https://unpkg.com/mermaid@8.0.0-rc.8/dist/mermaid.min.js'
|
@ -1,3 +0,0 @@
|
||||
<!-- start custom analytics snippet -->
|
||||
|
||||
<!-- end custom analytics snippet -->
|
@ -1,14 +0,0 @@
|
||||
{%- if site.analytics.google.tracking_id -%}
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={{ site.analytics.google.tracking_id }}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '{{ site.analytics.google.tracking_id }}');
|
||||
{% if site.analytics.google.anonymize_ip == true %}
|
||||
gtag('config', '{{ site.analytics.google.tracking_id }}', { 'anonymize_ip': true });
|
||||
{% endif %}
|
||||
</script>
|
||||
{%- endif -%}
|
@ -1,7 +0,0 @@
|
||||
{%- if jekyll.environment != 'development' -%}
|
||||
{%- if site.analytics.provider == 'google' -%}
|
||||
{%- include analytics-providers/google.html -%}
|
||||
{%- elsif site.analytics.provider == 'custom' -%}
|
||||
{%- include analytics-providers/custom.html -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
@ -1,55 +0,0 @@
|
||||
{%- include snippets/assign.html
|
||||
target=site.data.variables.default.page.show_author_profile
|
||||
source0=layout.show_author_profile source1=page.show_author_profile -%}
|
||||
{%- assign _show_author_profile = __return -%}
|
||||
|
||||
{%- include snippets/assign.html
|
||||
target=site.data.variables.default.page.show_subscribe
|
||||
source0=layout.show_subscribe source1=page.show_subscribe -%}
|
||||
{%- assign _show_subscribe = __return -%}
|
||||
|
||||
{%- include snippets/assign.html
|
||||
target=site.data.variables.default.page.license
|
||||
source0=layout.license source1=page.license -%}
|
||||
{%- assign _license = __return -%}
|
||||
|
||||
<footer class="article__footer">
|
||||
{%- if page.modify_date -%}
|
||||
{%- include snippets/get-locale-string.html key='ARTICLE_DATE_FORMAT' -%}
|
||||
{%- assign _locale_date_format = __return -%}
|
||||
|
||||
{%- include snippets/get-locale-string.html key='LAST_UPDATED' -%}
|
||||
{%- assign _locale_last_update = __return -%}
|
||||
<span>{{ _locale_last_update }}
|
||||
<time itemprop="dateModified" datetime="{{ page.modify_date | date_to_xmlschema }}">{{ page.modify_date | date: _locale_date_format }}</time>
|
||||
</span>
|
||||
{%- elsif page.date -%}
|
||||
<meta itemprop="dateModified" content="{{ page.date | date_to_xmlschema }}">
|
||||
{%- endif -%}
|
||||
|
||||
{%- include article/footer/custom.html -%}
|
||||
|
||||
{%- if _show_author_profile -%}
|
||||
{%- if page.author -%}
|
||||
{%- assign _author = site.data.authors[page.author] -%}
|
||||
{%- else -%}
|
||||
{%- assign _author = site.author -%}
|
||||
{%- endif -%}
|
||||
{%- include article/footer/author-profile.html author=_author -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if _show_subscribe -%}
|
||||
<div class="article__subscribe">{%- include article/footer/subscribe.html -%}</div>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if _license != false -%}
|
||||
{%- assign _data_license = site.data.licenses-%}
|
||||
{%- if site.license -%}
|
||||
{%- assign _license_data = _data_license[site.license] -%}
|
||||
{%- endif -%}
|
||||
{%- if _license != true -%}
|
||||
{%- assign _license_data = _data_license[_license] -%}
|
||||
{%- endif -%}
|
||||
<div class="article__license">{%- include article/footer/license.html license=_license_data -%}</div>
|
||||
{%- endif -%}
|
||||
</footer>
|
@ -1,49 +0,0 @@
|
||||
{%- include snippets/get-article-title.html article=include.article-%}
|
||||
{%- assign _article_title = __return -%}
|
||||
|
||||
{%- if include.html != false -%}
|
||||
|
||||
{%- include snippets/assign.html
|
||||
target=site.data.variables.default.page.show_title
|
||||
source0=layout.show_title source1=include.article.show_title -%}
|
||||
{%- assign _show_title = __return -%}
|
||||
|
||||
{%- include snippets/assign.html
|
||||
target=site.data.variables.default.page.show_edit_on_github
|
||||
source0=layout.show_edit_on_github source1=include.article.show_edit_on_github -%}
|
||||
{%- assign _show_edit_on_github = __return -%}
|
||||
|
||||
{%- if _show_title or _show_edit_on_github -%}
|
||||
<div class="article__header">
|
||||
{%- if _show_title -%}
|
||||
<header><h1>{{ _article_title }}</h1></header>
|
||||
{%- endif -%}
|
||||
{%- if _show_edit_on_github -%}
|
||||
{%- if site.repository and site.repository_tree -%}
|
||||
{%- include snippets/is_collection.html page=include.article -%}
|
||||
{%- assign _is_article_collection = __return -%}
|
||||
{%- include snippets/get-locale-string.html key='POST_ON_GITHUB' -%}
|
||||
{%- assign _locale_post_on_github = __return -%}
|
||||
{%- if _is_article_collection -%}
|
||||
{%- include snippets/prepend-path.html path=include.article.path prepend_path=site.collections_dir -%}
|
||||
{%- assign _article_path = __return -%}
|
||||
{%- else -%}
|
||||
{%- assign _article_path = include.article.path -%}
|
||||
{%- endif -%}
|
||||
{%- assign _github_path = site.repository | append: '/tree/' | append: site.repository_tree | append: '/' | append: _article_path | replace:'//','/' -%}
|
||||
<span class="split-space"> </span>
|
||||
<a class="edit-on-github"
|
||||
title="{{ _locale_post_on_github }}"
|
||||
href="https://github.com/{{ _github_path }}">
|
||||
<i class="far fa-edit"></i></a>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{%- else -%}
|
||||
<header style="display:none;"><h1>{{ _article_title }}</h1></header>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if include.semantic != false -%}
|
||||
<meta itemprop="headline" content="{{ _article_title }}">
|
||||
{%- endif -%}
|
@ -1,96 +0,0 @@
|
||||
{%- assign _author = site.data.authors[include.article.author] | default: site.author -%}
|
||||
|
||||
{%- if include.html != false -%}
|
||||
|
||||
{%- include snippets/assign.html
|
||||
target=site.data.variables.default.page.show_date
|
||||
source0=layout.show_date source1=include.article.show_date -%}
|
||||
{%- assign _show_date = __return -%}
|
||||
{%- if _show_date and include.article.date -%}
|
||||
{%- assign _show_date = true -%}
|
||||
{%- else -%}
|
||||
{%- assign _show_date = false -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- include snippets/assign.html
|
||||
target=site.data.variables.default.page.show_tags
|
||||
source0=layout.show_tags source1=include.article.show_tags -%}
|
||||
{%- assign _show_tags = __return -%}
|
||||
{%- if _show_tags and include.article.tags[0] -%}
|
||||
{%- assign _show_tags = true -%}
|
||||
{%- else -%}
|
||||
{%- assign _show_tags = false -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- assign _show_author = include.article.author -%}
|
||||
|
||||
{%- include snippets/assign.html target=site.data.variables.default.page.pageview
|
||||
source0=layout.pageview source1=page.pageview -%}
|
||||
{%- assign _pageview = __return -%}
|
||||
{%- if _pageview or include.show_pageview -%}
|
||||
{%- assign _pageview = true -%}
|
||||
{%- else -%}
|
||||
{%- assign _pageview = false -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- assign _paths_archive = site.paths.archive | default: site.data.variables.default.paths.archive -%}
|
||||
|
||||
{%- if _show_tags or _show_author or _show_date or _pageview -%}
|
||||
<div class="article__info clearfix">
|
||||
{%- if _show_tags -%}
|
||||
|
||||
<ul class="left-col menu">
|
||||
{%- assign _tag_path = _paths_archive | append: '?tag=' -%}
|
||||
{%- include snippets/prepend-baseurl.html path=_tag_path -%}
|
||||
|
||||
{%- for _tag in include.article.tags -%}
|
||||
{%- assign _tag_path = __return -%}
|
||||
{%- assign _tag_encode = _tag | strip | url_encode } -%}
|
||||
<li>
|
||||
<a class="button button--secondary button--pill button--sm"
|
||||
href="{{ _tag_path | append: _tag_encode | replace: '//', '/' }}">{{ _tag }}</a>
|
||||
</li>
|
||||
{%- endfor -%}
|
||||
</ul>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if _show_author or _show_date or _pageview -%}
|
||||
<ul class="right-col menu">
|
||||
{%- if _show_author -%}
|
||||
<li><i class="fas fa-user"></i> <span>{{ _author.name }}</span></li>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if _show_date -%}
|
||||
<li>
|
||||
{%- include snippets/get-locale-string.html key='ARTICLE_DATE_FORMAT' -%}
|
||||
<i class="far fa-calendar-alt"></i> <span>{{ include.article.date | date: __return }}</span>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if _pageview -%}
|
||||
{%- if site.pageview.provider -%}
|
||||
{%- include snippets/get-locale-string.html key='VIEWS' -%}
|
||||
{%- assign _locale_views = __return -%}
|
||||
<li><i class="far fa-eye"></i> <span class="js-pageview" data-page-key="{{ include.article.key }}">0</span> {{ _locale_views }}</li>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
</ul>
|
||||
{%- endif -%}
|
||||
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
|
||||
|
||||
{%- if include.semantic != false -%}
|
||||
{%- if _author -%}
|
||||
<meta itemprop="author" content="{{ _author.name }}"/>
|
||||
{%- endif -%}
|
||||
{%- if include.article.date -%}
|
||||
<meta itemprop="datePublished" content="{{ include.article.date | date_to_xmlschema }}">
|
||||
{%- endif -%}
|
||||
{%- if include.article.tags[0] -%}
|
||||
{%- assign _keywords = include.article.tags | join: ',' %}
|
||||
<meta itemprop="keywords" content="{{ _keywords }}">
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
@ -1,144 +0,0 @@
|
||||
{%- assign _excerpt_truncate = include.excerpt_truncate | default: 350 -%}
|
||||
|
||||
{%- assign _excerpt_type = include.excerpt_type -%}
|
||||
|
||||
{%- include snippets/get-locale-string.html key='READMORE' -%}
|
||||
{%- assign _locale_readmore = __return -%}
|
||||
|
||||
{%- assign _sorted_list = include.articles -%}
|
||||
{%- if include.group_by == 'year' -%}
|
||||
{%- assign _sorted_list = _sorted_list | sort: 'date' -%}
|
||||
{%- endif -%}
|
||||
{%- if include.reverse -%}
|
||||
{%- assign _sorted_list = _sorted_list | reverse -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if include.type == 'item' -%}
|
||||
<div class="article-list items items--divided">
|
||||
{%- elsif include.type == 'brief' -%}
|
||||
<div class="article-list items">
|
||||
{%- elsif include.type == 'grid' -%}
|
||||
{%- if include.size == 'sm' -%}
|
||||
<div class="article-list grid gird--sm grid--p-3">
|
||||
{%- else -%}
|
||||
<div class="article-list grid grid--p-3">
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- for _article in _sorted_list -%}
|
||||
|
||||
{%- include snippets/prepend-baseurl.html path=_article.url -%}
|
||||
{%- assign _article_url = __return -%}
|
||||
|
||||
{%- if _article.cover -%}
|
||||
{%- include snippets/get-nav-url.html path=_article.cover -%}
|
||||
{%- assign _article_cover = __return -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if include.type == 'item' -%}
|
||||
{%- if include.article_type == 'BlogPosting' -%}
|
||||
<article class="item" itemscope itemtype="http://schema.org/BlogPosting">
|
||||
{%- else -%}
|
||||
<article class="item">
|
||||
{%- endif -%}
|
||||
{%- if _article.cover and include.show_cover-%}
|
||||
{%- include snippets/get-nav-url.html path=_article.cover -%}
|
||||
{%- assign _article_cover = __return -%}
|
||||
<div class="item__image">
|
||||
{%- if include.cover_size == 'lg' -%}
|
||||
<img class="image image--lg" src="{{ _article_cover }}" />
|
||||
{%- elsif include.cover_size == 'sm' -%}
|
||||
<img class="image image--sm" src="{{ _article_cover }}" />
|
||||
{%- else -%}
|
||||
<img class="image" src="{{ _article_cover }}" />
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
<div class="item__content">
|
||||
<header><a href="{{ _article_url }}"><h2 itemprop="headline" class="item__header">{{ _article.title }}</h2></a></header>
|
||||
<div class="item__description">
|
||||
{%- if _article.excerpt and include.show_excerpt -%}
|
||||
<div class="article__content" itemprop="description articleBody">
|
||||
{%- if _excerpt_type == 'html' -%}
|
||||
{{ _article.excerpt }}
|
||||
{%- else -%}
|
||||
{{ _article.excerpt | strip_html | strip | truncate: _excerpt_truncate }}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- if include.show_readmore -%}
|
||||
<p><a href="{{ _article_url }}">{{ _locale_readmore }}</a></p>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{%- if include.show_info -%}
|
||||
{%- include snippets/assign.html target=site.data.variables.default.page.pageview
|
||||
source0=_article.pageview -%}
|
||||
{%- assign _show_pageview = __return -%}
|
||||
{%- include article-info.html article=_article show_pageview=_show_pageview -%}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
{%- elsif include.type == 'brief' -%}
|
||||
{%- assign _tags = '' -%}
|
||||
{%- for _tag in _article.tags -%}
|
||||
{%- assign _tag_encode = _tag | strip | url_encode -%}
|
||||
{%- if forloop.last -%}
|
||||
{%- assign _tags = _tags | append: _tag_encode -%}
|
||||
{%- else -%}
|
||||
{%- assign _tags = _tags | append: _tag_encode | append: ',' -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if include.group_by == 'year' -%}
|
||||
{%- assign _currentdate = _article.date | date: '%Y' -%}
|
||||
{%- if _currentdate != _date -%}
|
||||
{%- unless forloop.first -%}</ul></section>{%- endunless -%}
|
||||
<section><h2 class="article-list__group-header">{{ _currentdate }}</h2><ul class="items">
|
||||
{%- assign _date = _currentdate -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- include snippets/get-locale-string.html key='ARTICLE_LIST_DATE_FORMAT' -%}
|
||||
<li class="item" itemscope itemtype="http://schema.org/BlogPosting" data-tags="{{ _tags }}">
|
||||
<div class="item__content">
|
||||
{%- if include.show_info -%}
|
||||
<span class="item__meta">{{ _article.date | date: __return }}</span>
|
||||
{%- endif -%}
|
||||
<a itemprop="headline" class="item__header" href="{{ _article_url }}">{{ _article.title }}</a></div>
|
||||
</li>
|
||||
|
||||
{%- elsif include.type == 'grid' -%}
|
||||
{%- if include.size == 'sm' -%}
|
||||
<div class="cell cell--12 cell--md-4 cell--lg-3">
|
||||
<div class="card card--flat">
|
||||
{%- if _article.cover -%}
|
||||
<div class="card__image">
|
||||
<img class="image" src="{{ _article_cover }}" />
|
||||
<div class="overlay overlay--bottom">
|
||||
<header>
|
||||
<a href="{{ _article_url }}"><h2 class="card__header">{{ _article.title }}</h2></a>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</div>
|
||||
{%- else -%}
|
||||
|
||||
<div class="cell cell--12 cell--md-6 cell--lg-4">
|
||||
<div class="card card--flat">
|
||||
{%- if _article.cover -%}
|
||||
<div class="card__image"><img src="{{ _article_cover }}" /></div>
|
||||
{%- endif -%}
|
||||
<div class="card__content">
|
||||
<header>
|
||||
<a href="{{ _article_url }}"><h2 class="card__header">{{ _article.title }}</h2></a>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
|
||||
</div>
|
@ -1,54 +0,0 @@
|
||||
{%- if page.sidebar.nav -%}
|
||||
{%- assign _sidebar_nav = site.data.navigation[page.sidebar.nav] -%}
|
||||
{%- if _sidebar_nav -%}
|
||||
{%- assign _find_cur = false -%}
|
||||
{%- assign _find_next = false -%}
|
||||
|
||||
{%- for _item in _sidebar_nav -%}
|
||||
{%- if _find_next -%} {%- break -%} {%- endif -%}
|
||||
{%- if _item.children -%}
|
||||
|
||||
{%- for _child in _item.children -%}
|
||||
{%- include snippets/get-nav-url.html path=_child.url -%}
|
||||
{%- assign _nav_url = __return -%}
|
||||
{%- include snippets/get-nav-url.html path=page.url -%}
|
||||
{%- assign _page_url = __return -%}
|
||||
|
||||
{%- if _nav_url == _page_url -%}
|
||||
{%- assign _find_cur = true -%}
|
||||
{%- elsif _find_cur and _find_next != true -%}
|
||||
{%- assign _find_next = true -%}
|
||||
{%- assign _next = _child -%}
|
||||
{%- break -%}
|
||||
{%- else -%}
|
||||
{%- assign _previous = _child -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- else -%}
|
||||
{%- assign _previous = page.previous -%}
|
||||
{%- assign _next = page.next -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if _next or _previous -%}
|
||||
<div class="article__section-navigator clearfix">
|
||||
{%- if _previous -%}
|
||||
{%- include snippets/prepend-baseurl.html path=_previous.url -%}
|
||||
{%- assign _href = __return -%}
|
||||
{%- include snippets/get-locale-string.html key='PREVIOUS' -%}
|
||||
{%- assign _locale_previous = __return -%}
|
||||
<div class="previous"><span>{{ _locale_previous }}</span><a href="{{ _href }}">{{ _previous.title }}</a></div>
|
||||
{%- endif -%}
|
||||
{%- if _next -%}
|
||||
{%- include snippets/prepend-baseurl.html path=_next.url -%}
|
||||
{%- assign _href = __return -%}
|
||||
{%- include snippets/get-locale-string.html key='NEXT' -%}
|
||||
{%- assign _locale_next = __return -%}
|
||||
<div class="next"><span>{{ _locale_next }}</span><a href="{{ _href }}">{{ _next.title }}</a></div>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{%- endif -%}
|
@ -1,47 +0,0 @@
|
||||
{%- assign _author = include.author -%}
|
||||
|
||||
{%- if _author.type == 'organization' -%}
|
||||
{%- assign _author_itemtype = 'http://schema.org/Organization' -%}
|
||||
{%- else -%}
|
||||
{%- assign _author_itemtype = 'http://schema.org/Person' -%}
|
||||
{%- endif -%}
|
||||
|
||||
<div itemscope itemtype="{{ _author_itemtype }}" class="author-profile card card--flat item">
|
||||
{%- if _author.avatar -%}
|
||||
{%- if _author.url -%}
|
||||
<a href="{{ _author.url }}" class="item__image">
|
||||
{%- endif -%}
|
||||
{%- include snippets/get-nav-url.html path=_author.avatar -%}
|
||||
{%- assign _author_avatar = __return -%}
|
||||
<img class="author-profile__avatar" itemprop="image" src="{{ _author_avatar }}" />
|
||||
{%- if _author.url -%}
|
||||
</a>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
|
||||
|
||||
<div class="item__content">
|
||||
|
||||
{%- if _author.name -%}
|
||||
<meta itemprop="name" content="{{ _author.name }}">
|
||||
<p class="author-profile__name">
|
||||
{%- if _author.url -%}
|
||||
<meta itemprop="url" content="{{ _author.url }}">
|
||||
<a href="{{ _author.url }}">
|
||||
{%- endif -%}
|
||||
{{ _author.name }}
|
||||
{%- if _author.url -%}
|
||||
</a>
|
||||
{%- endif -%}
|
||||
</p>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if _author.bio -%}
|
||||
<p itemprop="description">{{ _author.bio }}</p>
|
||||
{%- endif -%}
|
||||
<div class="author-profile__links">
|
||||
{%- include author-links.html author=_author -%}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -1,3 +0,0 @@
|
||||
<!-- start custom article footer snippet -->
|
||||
|
||||
<!-- end custom article footer snippet -->
|
@ -1,14 +0,0 @@
|
||||
{%- include snippets/get-locale-string.html key='LICENSE_ANNOUNCE' -%}
|
||||
{%- assign _license_announce = __return -%}
|
||||
|
||||
{%- if include.license -%}
|
||||
|
||||
{%-assign _license_name = '<a itemprop="license" rel="license" href="[URL]">[NAME]</a>' | replace: "[URL]", include.license.url | replace: "[NAME]", include.license.name -%}
|
||||
<div class="license">
|
||||
<p>{{ _license_announce | replace: "[LICENSE]", _license_name }}
|
||||
<a rel="license" href="{{ include.license.url }}">
|
||||
<img alt="{{ include.license.name }}" src="{{ include.license.image }}" />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{%- endif -%}
|
@ -1,6 +0,0 @@
|
||||
{%- assign _paths_rss = site.paths.rss | default: site.data.variables.default.paths.rss -%}
|
||||
{%- include snippets/get-nav-url.html path=_paths_rss -%}
|
||||
{%- assign _paths_rss = __return -%}
|
||||
{%- include snippets/get-locale-string.html key='SUBSCRIBE' -%}
|
||||
{%- assign _locale_nav_subscribe = __return -%}
|
||||
<div class="subscribe"><i class="fas fa-rss"></i> <a type="application/rss+xml" href="{{ _paths_rss }}">{{ _locale_nav_subscribe }}</a></div>
|
@ -1,3 +0,0 @@
|
||||
<!-- start custom article top snippet -->
|
||||
|
||||
<!-- end custom article top snippet -->
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user